DeepSeek Harness 进阶:给 Agent 立个目标,自动续跑 256 轮
生命不息,折腾不止。上一课解决了「上下文涨满」,这一课解决「活干到一半 Agent 失忆」:给你的 Agent 立一个跨会话、跨重启都不丢的目标,它就能自己一鼓作气续跑最多 256 轮。
一、目标不是「待办清单」,是「方向盘」
先纠正一个最常见的第一反应:很多人以为 dsh 里的 goal(目标) 就是让 Agent 记一张 todo 清单,跑到一半断了重来就行。不是。goal 是一个会话里的长期完成目标:目标文本、当前阶段(phase)、已经跑了多少轮、每一次改动的修订历史,全部写进 append-only 的会话日志里。所以会话恢复(resume)、会话分叉(fork)、甚至进程重启,目标都原封不动还在。
但这里有个最容易踩、也最关键的分工,务必先记住:
dsh-goal 只存状态,不调度工作。 它是「方向盘」,不是「发动机」。
真正让 Agent 一轮接一轮自动往下跑的,是另一个包 goal-round-driver(续跑驱动)。理解了「状态」和「调度」是两拨人各管一摊,整个 goal 体系就通了。整个体系一共四个包:
| 包 | 角色 | 一句话 |
|---|---|---|
dsh-goal |
状态核心 | 存目标、管阶段、记历史,只存不跑 |
dsh-tool-goal |
模型的手 | 让模型能读/建/改目标(get_goal 等工具) |
dsh-command-goal |
人的手 | 给你一个 /goal 命令直接操作 |
dsh-goal-round-driver |
发动机 | Agent 空闲时自动排下一轮,直到跑满/完成/被拦 |
一个会话最多同时有一个「当前目标」:没跑完的目标得先 edit / pause / resume / block / clear 掉,才能换下一个;只有已经 complete 的目标可以直接被新目标顶替。这保证了「长期目标」唯一的语义不会乱。
二、四个包怎么搭(composition)
目标这块是「一切皆插件」,所以装配方式就是往 composition 里加条目。以官方 README 的写法为准,四件套长这样:
1 | - id: goal |
几个要点:
defaultMaxGoalRounds: 256是默认轮次上限,只有「创建时没显式指定上限」的目标才用它;创建时自己带了值的,以自带为准。它必须是个正整数。blockedAfterConsecutiveRounds: 3是模型的阻塞阈值:同一个阻塞条件要连续 3 轮都出现,模型才允许报blocked。dsh-command-goal注册/goal命令依赖命令系统dsh-commands;dsh-goal本身要依赖会话投影dsh-session-projection。用官方npx @deepseek-ai/dsh web的默认组合时这些通常都在列,自己精装配时别忘了带上,否则ctx.goals可能挂不上。- 注意一个易错点:轮次上限写在 goal 定义里,阻塞阈值写在 tool-goal 里,别在
goal-round-driver里再配一份——那一层本身没有任何配置项,重复值只会造成两头策略不一致。
三、人怎么用:/goal 命令(你的手)
挂上 dsh-command-goal 后,你就能在交互界面直接敲命令控制目标,不用绕模型。命令和它的直接输出只留在界面里,不会塞进模型请求(省钱、也不干扰模型)。
| 输入 | 结果 |
|---|---|
/goal |
显示当前目标、阶段、轮次进度(x/上限)、是否 armed,以及下一步可用命令 |
/goal <目标内容> |
创建并 arm 一个新目标;或顶替一个已完成的目标 |
/goal edit <目标内容> |
改目标文本,不改阶段、不改激活状态 |
/goal pause |
暂停并解除激活(disarm) |
/goal resume |
恢复 / 在会话恢复或分叉后重新 arm(受剩余轮次上限约束) |
/goal clear |
清除当前目标(历史仍保留在会话日志里) |
这里有一个特别阴的语法坑,实测很容易中招:控制词 clear / pause / resume / edit 只有在「整段输入就这一个词」时才被认成命令。除此之外的任何非空后缀都会被当成目标文本。也就是说:
/goal pause→ 暂停 ✅/goal pause after verification→ 会创建一个字面内容为「pause after verification」的目标 ❌(不是「暂停」)
所以控制命令后面千万别手贱加注释。edit 单独敲(不带参数)会直接返回一个脚本检查错误,等于提醒你「正文呢」。
另外,/goal 支持附件:创建或编辑时选中的图片/文件,会被合成一条普通用户消息,附上固定文案 Reference attachments for the goal objective.,后续目标轮次能当普通历史读到它——附件的状态由目标域保存,不是单独存。
四、模型怎么用:三个工具 + 乐观锁
模型那一侧由 dsh-tool-goal 提供三个工具,返回的都是一段紧凑 JSON:
1 | { "goal": null } |
或者有目标时:
1 | { |
三个工具分别是:
get_goal()—— 读当前目标,没有就返回null。create_goal(objective, max_goal_rounds?)—— 从一条「顶层人类直接请求」里创建目标(可推断意图,任何语言都行)。update_goal(goal_id, revision, action, objective?, max_goal_rounds?, blocked_reason?)—— 对「精确的某个修订」执行edit/pause/resume/complete/blocked。
这里有两个必须懂的机制:
第一,乐观锁(compare-and-set)。 每次 update_goal 都要带上 get_goal 拿到的精确 goal_id 和 revision。谁手里的状态旧了,谁就会收到一个清晰的「stale revision」错误,而不是悄无声息把别人的新进度覆盖掉。这就是为什么官方强调「先 get 再 update,抄下精确的 id 和 revision」。
第二,权限边界。 create / edit / pause / resume 这四类写操作,必须来自一个「顶层人类消息」(runtime-root agent 的当前轮)——子 agent 或非人类生产者建不了、也改不了目标。complete 和 blocked 才允许出现在自主目标轮里。而 resume 会拒绝一个持久化 paused 的目标(返回 GOAL_TOOL_RESUME_PAUSED),那种状态归用户侧的 /goal resume 管,模型插不了手。
顺带说一声:自主轮里成功报出 complete 或 blocked,那一轮会跟着结束物理 turn,模型会收到一条收尾指令,把最终话写给用户——所以模型不会「报完完成还继续瞎跑」。
五、发动机怎么转:goal-round-driver
这是整套里最「Agent」的部分。它没有配置项,触发续跑要同时满足三个条件:
- Agent 整机空闲;
- 目标处于 active 且 armed;
- 剩余轮次 > 0。
满足后它就自动排队一条 goal-round 提示,格式是一段文字:开头是 JSON 引号包围的目标内容 + round/maxGoalRounds,后面跟工作指令,让模型「以当前工作区、工具结果、持久化状态为权威」继续推进。每轮作为一个 goal 来源的用户消息进入历史。只有真正进入模型历史的那条 goal 消息才消耗轮次配额——人类消息、被判定为失效的占位都不会扣。
续跑会停在这几种情况:目标被 complete / pause / block;一轮在 max tokens 上截断;持久化写失败;agent 被 cancel;插件被卸载;轮次上限耗尽(这时会记录一个稳定码为 round-limit 的阻塞)。
全篇最重要、也最容易被忽略的一个安全设计在这:
会话恢复、分叉、或重新挂载驱动之后,一个 active 目标会自动 disarm,不会自己接着跑。
英文原文叫「an active goal is disarmed after any session-start edge」。也就是说,你半夜关掉进程,第二天早上用 resume 把它接回来,它不会一睁眼就自作主张继续 hammer 你的工具——必须你来一次显式 resume(或 /goal resume),它才重新 arm 接着干。这个「自觉停火」是刻意的:自动续跑的授权是进程内局部的,从不持久化,谁也别想靠「重放历史」把一个会话恢复成无人值守的续跑机。
还有个贴心点:cancel 操作也不会自动重来一轮——目标轮如果正在跑被 cancel,会在下一个空闲点停下来变 paused;跟目标无关的 cancel 只是 disarm,不会偷偷重排。
六、踩坑清单(都是实测/文档里明说的)
- 控制词要整段:
/goal pause 等验证完不是暂停,是建了个奇葩目标。 - 子 agent 建不了目标:create/edit/pause/resume 必须顶层人类消息发起,别指望子 agent 帮你立大旗。
- block 要「连续 3 轮同一条件」:难度大、不确定、还有活没干完,统统不算 block,别让模型早退。
- resume 分两种人:模型
update_goal resume拒绝持久化 paused(GOAL_TOOL_RESUME_PAUSED),那种得你亲手/goal resume。 - 256 只是「轮次」预算,不是钱包:它不计量 token、不计量美元、也不计量 provider 配额,别拿它当成本上限。
- 别在 goal-round-driver 里重复配轮次/阈值:两个值分属 goal 和 tool-goal,驱动层零配置,重复配会两头打架。
- attachments 不单独持久化:附件是合成进普通历史让目标轮读的,目标域本身不存附件状态。
生命不息,折腾不止。目标体系里还剩一块没讲:那些「又慢又可以后台跑」的活怎么办?下一篇钻进 dsh 的 后台 job——
run_in_background怎么把重活甩到后台不堵主循环、job_output/job_list/job_kill怎么随时查看和掐掉,配合 goal 一起用,长任务才真正「放得下、收得回」。