DeepSeek Harness 无头自动化:一条命令让 Agent 自己跑批量任务
生命不息,折腾不止。这一篇把「人工守着点」变成「写完睡觉、明早收结果」——不开浏览器、不起服务,一条命令让 dsh 的 Agent 自己把活干完。
前面几篇我们一直在 Web 界面里点来点去玩 dsh(DeepSeek Harness),今天聊点真正能落地的:无头模式(headless)。它干的事特别朴素——跑一个任务、把最后一段回答打到终端、然后退出。就这三个动作,却正好是自动化(cron、CI、shell 脚本)要的形状。上个月就有不少兄弟问我「能不能让它半夜自己干活」,答案全在这一篇。
一、先想明白:dsh 不是「聊天终端」,它是个 profile 启动器
很多人以为 dsh 命令是个类似终端聊天(TUI)的东西,其实不是。官方对它的定位很明确:dsh 是一个 profile 启动器,它本身不管交互,只负责把你交给某个「profile」(配置组合)去跑。它有四个入口:
| 命令 | 干什么 |
|---|---|
dsh --profile <name> |
启动 $DSH_HOME/profiles/<name> 这个 profile |
dsh --profile headless "任务" |
跑一次性任务,执行完退出 |
dsh web |
--profile web 的硬编码别名(就是大家天天用的 Web 界面) |
dsh plugin --profile <name> <参数> |
管理 profile 的插件依赖(转发给 pnpm) |
注意第 2 个。headless 和 web 一样,都是内置保留的 profile 名,第一次用会自动从随附模板初始化——web = base + web-app,headless = base + headless。
关键差别在于:headless 这个 bundle 不挂 ApiProxy、HTTP 服务、Web runtime,也不带浏览器客户端。翻译成人话就是——它一个监听端口都不开,没有东西会被意外暴露到网络上,启动开销也小一圈。而文件编辑、命令执行、AGENTS.md/CLAUDE.md 上下文加载(有 65,536 字节的预算上限)、工作区写沙箱这些,跟 web 模式完全一致。
还有一个容易踩的坑:老教程里的 dsh run 子命令已经移除了,一次性任务统一改成 dsh --profile headless "任务"。别照着旧资料抄 dsh run。
二、跑起来:第一条 headless 命令
前置环境跟装 dsh 时一样:Node.js ^22.19.0 或 >=24.0.0。零安装直接:
1 | npx @deepseek-ai/dsh --profile headless "把当前目录的测试跑一遍并汇总结果" |
引号里那一串就是任务本身,是位置参数,不是 flag。它背后发生的事是这样的:
- 建一个全新的持久化 Session;
- 把你这句话当成一条用户消息提交进去;
- 等 Agent 跑到「安静下来」(quiescence);
- flush 会话、把最后一段非空的助手文本打到 stdout;
- 退出。
所谓「一次性」,指的是一个任务,而不是「一次模型调用」——Agent 在内部可以自己决定读多少文件、调多少工具、甚至派几个子 Agent,这中间可能有几十次调用。所以任务文本写得好不好,直接决定结果靠不靠谱。反例:「帮我修测试」太含糊;正例:「tests/orders 里的测试挂了,找出根因、修生产代码、别动测试文件本身」。
来看几个能直接抄的真实用例:
1 | # 让 Agent 总结当前仓库,只读不写 |
三、退出码是自动化的「契约」,得看懂
headless 模式之所以适合塞进脚本,靠的就是退出码。官方给的契约很清晰:
| 退出码 | 含义 |
|---|---|
0 |
最终 turn 的结束原因是 completed(任务正常完成) |
1 |
没到 completed,或者 runner 本身挂了 |
130 |
收到第一个 SIGINT(Ctrl+C)后优雅关闭 |
有两个坑要专门提:
- 退出码 0 只证明「Agent 跑完了」,不证明你要的文件真的存在、部署真的成功、测试真的过了。外部效果得自己另外验证。
- SIGTERM 会被当成正常的停止请求,任何情况下都退出 0。如果你用 systemd、CI 的超时机制去杀它,超时杀掉和「正常完成」在退出码上分不出来,得另外看任务输出/状态来判断。
写个最朴素的 shell 包装器,靠退出码决定脚本走向:
1 |
|
四、让它半夜自己干活:环境变量 + cron
headless 从环境变量里读凭据和配置,这正是它「流水线友好」的原因——没有交互式弹窗、没有 keyring、不会把配置文件误提交进仓库。常用的几个:
1 | export DSH_HOME=/custom/path # harness 主目录,默认 ~/.dsh |
(这些变量名是社区教程里的通用约定,具体以官方文档为准。)其中 DSH_MODEL 钉死模型这件事,我建议当成非协商项——不然哪天服务商改了默认模型,你半夜那条 cron 的行为和账单就悄悄变了。
DEEPSEEK_BASE_URL 指向任何 OpenAI 兼容接口都能跑,这正是 dsh「多模型对接 + 价格便宜」的卖点。想换成 Claude/GPT 这类国外模型,把 BASE_URL 指到中转站 ai.aklibk.com 就行(国内直连、免绑卡、人民币按量付费),前几篇《自定义 Provider》有完整步骤。
一条夜里跑代码审查的 cron,长这样:
1 | 0 2 * * * cd /srv/my-app && \ |
几个要点:cd 到正确的目录再启动——headless 和 web 一样,把「当前工作目录」当成默认的工作区根,而且任务里没法事后把 Session 挪到更安全的仓库去;日志就落到上一篇讲的会话 JSONL 里(~/.dsh/sessions/),等于自动留了一份可追溯的「账本」,出问题几周后都翻得出来。
五、插件是分 profile 的,headless 里要单独装
这是最容易被忽略的一个坑:你在 web 界面里装的插件,headless profile 里不会自动有。插件是 profile 作用域的,得单独给 headless 装:
1 | dsh plugin --profile headless add @deepseek-ai/dsh-subagent-codex |
机制跟 Web UI 一模一样,同一个插件市场、同一套版本解析。所以下次你发现「插件在 web 里好用、cron 里没生效」,先别怀疑人生,dsh plugin --profile headless add <包名> 补一遍再说。
六、写在最后
无头模式是 dsh 里过了演示阶段之后最有用的一个模式——它把「人」从循环里拿掉,换成退出码和日志文件,于是 Agent 从「玩具」变成了能进 cron、进 CI、进脚本的「零件」。但也要清醒:它是一次性的、没有中途纠偏的机会,含糊的任务 + 没人在场 = 凌晨三点自信满满地跑偏。所以任务文本写得越具体、模型钉得越死,它越像个靠谱的夜班同事。
生命不息,折腾不止。下一篇我把这个系列收个口——《DeepSeek Harness 速查表:模型、插件、沙箱、权限、Creator、Trajectory、无头自动化一条命令清单》——把这一路所有命令和配置串成一张能收藏的 cheat sheet,想不起来的时候翻一眼就够。