生命不息,折腾不止。让 Agent 把耗时命令丢进后台,既不傻等也不被超时打断,边干别的边回收结果——dsh 的后台任务三件套一次讲透。

上一课我们给 Agent 立了个「goal」,它能自己续跑 256 轮把目标干完。但干到一半有个尴尬:很多活是慢工出细活,比如 npm run build 一跑三分钟、压测要挂半小时、批量下载几千个文件——你要是让 Agent 用前台命令死等,要么撞上超时被砍,要么它就对着转圈啥也不干。今天这一课,就把 dsh 的**后台任务(background job)**机制讲清楚:怎么丢后台、怎么盯进度、怎么喊停,以及它背后那套「通知识别」的设计。

一、为什么 Agent 非得会「丢后台」

默认情况下,dsh 里的命令工具(bash、终端)都是前台同步执行:Agent 发一条命令,就得死等它跑完拿输出,期间它什么也干不了。这对短命令没问题,对长任务就两个坑:

  1. 撞超时:前台命令有超时上限,跑不完就被打断,Agent 还以为是命令失败了。
  2. 干等:任务跑着,Agent 只能原地转圈,纯浪费一轮又一轮。

后台任务就是来解决这个的。dsh 的设计里,把「后台」抽象成了一个统一的任务(job)概念——不管是后台一条 bash 命令、终端里的一次交互,还是派出去的子代理(subagent),都是同一个东西,用同一套三件套工具去读、去列、去停。这套能力由 @deepseek-ai/dsh-tool-jobs 这个插件提供,它是 dsh 官方 jobs/ 包组里面向模型的那半边。

简单记住一句话:「丢后台」是动作,「job」是句柄,三件套是操控它的遥控器。 下面拆开讲。

二、怎么把命令丢后台:run_in_background 一个参数的事

想让一条命令进后台,不用另起一套 API,就是在原来那套命令工具里加一个布尔参数 run_in_background。这个参数由三个「生产方」暴露:bash 工具、terminal 工具、还有子代理工具。它们各自用 enableRunInBackground 开关控制要不要这本事,默认是开的。

拿 bash 工具举例,它的参数结构大概长这样(示意,以官方文档为准):

1
2
3
4
5
{
"command": "npm run build",
"description": "构建前端",
"run_in_background": true
}

把 run_in_background 设成 true,这条命令就立刻返回一个 job id,Agent 马上能继续干别的活,不用等它跑完。而且后台任务没有超时——这正是它跟前台命令最本质的区别:长任务终于不怕被砍了。

对你(用户)来说,你甚至不用自己写这个 JSON。dsh 是网页交互(npx @deepseek-ai/dsh web),你只要用大白话告诉它:

「把这个 npm run build 丢后台跑,跑完告诉我结果。」

Agent 自己会在调用命令工具时把 run_in_background 置为 true,拿到 job id,回头再回来收结果。

三、回收三件套:job_output / job_list / job_kill

任务丢进后台了,怎么管?就这仨工具,名字直白得不行:

1. job_output(job_id, wait?, timeout_ms?) —— 读输出

按 job id 去取某个任务的输出。这里有个关键行为要拎清楚:后台任务分两种「口径」——

  • 流式任务(stream):像终端里持续吐输出的那种,每次 job_output 只返回上次读完之后的新增量,读一次就消费掉,不会重复吐。
  • 最终结果型(final-output):像子代理那类,跑完才结算,job_output 在它 settle 之后把最终结果一次性给你。

不管哪种,每次响应的结尾都会带一个 [status: ...],告诉你任务现在的状态(running、finished、killed 之类),这个尾巴很重要,别忽略。

另外它默认是非阻塞的:一调就回,没新输出就告诉你「暂无」。想让 Agent 等它,得显式带 wait: true,还可以加 timeout_ms 控制最多等多久。注意 wait 超时并不会把任务弄死,只是返回个 [status: running],任务照旧活着等下次再读。

1
{ "job_id": "job_abc123" }
1
{ "job_id": "job_abc123", "wait": true, "timeout_ms": 5000 }

2. job_list() —— 列出所有活

不带参数,一调就把当前会话能看到的后台任务全列出来,一行一个,格式是:

1
<id> [<kind>] <status> — <label>

比如:

1
2
job_abc123 [bash] running — 构建前端
job_def456 [subagent] finished — 查三地手续费

一个任务都没有时,它会返回 (no background jobs)。这个工具适合 Agent 断点续跑或重启会话后,先盘一盘「我还有哪些活在跑」。

3. job_kill(job_id, reason?) —— 喊停

任务跑偏了、不需要了,用 job id 取消。它会立即请求取消,等任务真正停下来了,状态会落到 killed。可选参数 reason 会记进任务详情,方便后面追查。返回值是 { outcome: 'cancellation-requested' | 'already-finished', job }——要么「已请求取消」,要么「它早就结束了」。

1
{ "job_id": "job_abc123", "reason": "改走新方案了" }

四、完成通知:Agent 是被「叫醒」的,不是自己轮询

这套机制最聪明的地方,在于Agent 根本不用傻乎乎地轮询。官方系统提示里明确写着:别对一个任务「忙轮询」或 sleep,通知是自动来的。

当一个后台任务 settle(跑完/被杀)时,dsh 会给「任务的主人」发一条会话内通知,格式长这样:

1
background job <id> (<kind>: <label>) finished [status: ...]. Read its output with job_output.

这条通知走哪条「车道」,取决于 Agent 现在忙不忙:

  • 忙着的 Agent(正在某一步里干活):通知被塞进它下一步的收件箱,而且这步没空出来之前,turn 不能关闭——所以几个任务一起跑完,只花一步就全交代了,不会每个任务各开一顿新会话。
  • 闲着的 Agent(已经答完了、在等你说下一句):dsh 会主动「叫醒」它,额外开一个新 turn 让它去收结果。这很重要——不然一个没人认领的「完成通知」,就是 Agent 永远也不会知道的完成。

但「叫醒」不是无限的。这里有个防自激的设计:每个 owner 能被连续叫醒的次数是有上限的(maxConsecutiveWakes,早期版本默认 3)。你可能会问为啥要限——因为「叫醒」这个动作本身就是一条通知,Agent 被叫醒后很可能又启动一个后台任务,那个任务又跑完又来叫醒它……不设个上限,链式自激能把你的 token 烧穿。这个上限不随时间恢复,只有你(用户)主动发一条消息才会重置。 想让通知老老实实留在收件箱、不额外花钱开 turn,就把 completionDelivery 改成 quiet(确定性回放/调试场景常用)。

五、配置项与踩坑清单

最小配置:默认就能用

加载这个插件的常见姿势就是什么都不配:

1
- name: '@deepseek-ai/dsh-tool-jobs'

要调参,可配置项就这几个:

键 默认值 含义
waitTimeoutMs 30000 wait: true 但没给 timeout_ms 时的默认等待毫秒数
maxWaitTimeoutMs 600000 模型自己传的等待上限,超了会被压到这个值
completionDelivery wakeup wakeup 会叫醒空闲的 owner;quiet 让通知留在收件箱
maxConsecutiveWakes 3(新版可能不设上限) 一个 owner 能连续被叫醒的次数,之后降级为注入收件箱

有个硬约束:waitTimeoutMs 配得比 maxWaitTimeoutMs 还大,插件会直接加载失败。配的时候别写反了。

踩坑清单(都是官方 README 里点名的)

  1. 没加载 tool-jobs,后台任务根本起不来。这个插件的 controller 是「armed」生产方 ctx.jobs.start() 的那一环——你就算在别处配了 bash 工具,缺了它,run_in_background 也是哑的。别以为「跑后台」是 bash 工具自带的能力。

  2. wait 超时 ≠ 任务失败。超时会返回 [status: running],任务还活着。别看到「超时」就认定失败了去重跑,会跑重复的活。

  3. 模型读取是「单消费者」。流式任务的输出,Agent 读一次就消费掉了,别的观察者再去读拿不到同一段。多个人同时盯着同一个任务输出时,别指望人人分到一份。

  4. 空闲 owner 身上的「待发通知」活不过 owner 被销毁。会话一关闭/销毁,没认领的通知就没了。所以别指望挂一晚上回来还能看到「完成提醒」,回来用 job_list 主动盘一遍最稳。

  5. 叫醒预算不靠时间回血。上面说过了,maxConsecutiveWakes 用光了,只有你发消息才能补。无人值守长跑要注意别让 Agent 干等着一条永远不会主动来的通知(这里跟第 3 课讲的 goal 续跑正好衔接:goal 会轮询,后台任务会通知,两者各管一摊)。

官方给模型的行为建议(顺手贴给你,理解它的「工作节律」)

1
Track every background job id you start. You are notified in-session when a job finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running job's work. Before giving a final answer, collect every still-relevant job with job_output (set wait: true only when you are genuinely blocked on it), and job_kill jobs that stopped mattering.

翻译成人话:记住你丢出去的每个 job id;完成会自动通知你,别忙轮询;趁任务跑着去干别的独立步骤;最终回答前,把还相关的任务用 job_output 收一遍(只有真被卡住了才用 wait: true),把已经没用的任务用 job_kill 清掉。

小结

后台任务是 dsh 里「让 Agent 更像一个靠谱打工人」的关键一块拼图:前台命令管短平快,后台任务接管一切耗时的、要并行的、要慢慢盯的活。配合前几课的钩子、事件流、压缩、目标续跑,你现在已经有了一套完整的「长程自动干活」工具箱了。

下一篇,我们顺着「后台任务」这条线往下走一层——子代理(subagent):刚才反复提到的「派出去的 subagent」到底怎么派、主 Agent 怎么汇总、多个子代理怎么协作。这也是后台任务三件套里 kind 字段里那个神秘 subagent 的真身,别错过。

生命不息,折腾不止。下一篇《DeepSeek Harness 进阶:派个子代理去干活,主 Agent 坐镇汇总》,把 dsh-tool-subagent 的派发、结果回传、与后台任务三件套的联动一次说透。