☰
dsh不内置Cron?三种方案给Agent加定时任务
2026/10/3 5:36:13 网站建设 项目流程

刚接手 dsh 的时候我其实挺不习惯的。手里正好有个需求,想让 Agent 每天早上九点自动汇总一下团队日报并生成摘要,打开 Tools 列表一看,傻眼了,整个框架就给了三个工具,翻遍文档也没有 Cron 相关的配置项。在社区里问了一圈,得到的答复更有意思:作者是故意不做 Cron 的。这句话一下子把我点醒了。这篇文章就跟大家聊聊,我是怎么理解 dsh 这个"三个工具"的定位,又是怎么在不借助内置 Cron 的情况下,给 Agent 加上稳定的定时任务能力的。

适合谁来读呢?一是刚接触 dsh、正在纠结"定时任务怎么加"的 Agent 开发者,二是所有在 Agent 框架选型时纠结"要不要内置调度"的人。我会把设计思路、三种可落地的方案、完整实操过程和踩坑记录都写出来,尽量做到看完就能直接抄作业。

1. dsh 只给三个工具,背后是一套完整的设计哲学

1.1 三个工具意味着什么

很多 Agent 框架一上来就是几十个 Tools,读文件的、写数据库的、调第三方 API 的、发邮件的,琳琅满目。dsh 反其道而行,默认只保留三个核心工具。我一开始觉得这太抠了,但实际用下来才意识到,这恰恰是作者对"Agent 能力边界"的一次刻意收敛。

三个工具的组合,按常见实践来推演,应当是模型对话、文件读写和指令执行三件套。模型对话负责理解用户意图,文件读写负责持久化上下文,指令执行负责把意图落地成真实操作。这是一个 Agent 能跑起来的最小闭环:想、记、做。任何超出这个范围的能力,dsh 都点名了,你自己通过插件挂载。

这个设计理念其实很不一般。它默认使用 dsh 的人是懂 Agent 的,懂"工具不是越多越好",懂"能力应该按需加载"。工具集一旦膨胀,Agent 的决策空间就会变得非常不稳定,模型在几十个工具里做路由,出错概率是指数级上升的。三个工具的本质是给 Agent 划了一条清晰的能力边界,边界之外的东西,你可以随时扩展,但不会被默认塞满。

1.2 "故意不做 Cron"恰恰是正确的设计

Cron 的全称是 Chronos,在 Unix 世界里它几乎和"定时任务"画等号。任何一个框架缺少 Cron,第一反应都是功能缺失。但 dsh 的取舍点就在这里:Agent 的自主决策和 Cron 的固定执行计划,本质上是一种世界观冲突。

Cron 是确定性的执行计划,它不关心任务的意义、上下文和优先级,只关心"到了这个时间点,给我执行这一条命令"。而 Agent 的核心能力恰恰是自主判断——什么时候该做什么事、做到什么程度、遇到异常怎么调整。如果你给 Agent 强行加一个 Cron,等于默认 Agent 的"决策权"是假的,它最终还是会退化成一台按照预置时间表滚动运行的脚本机。

我理解 dsh 作者是故意的。他没有把事情做绝,只是把"何时执行任务"这一层从框架里剥了出去,交给使用者自己决定。这非常符合 Unix 哲学里的那句经典表述:一个程序只做好一件事。dsh 负责 Agent 的运行逻辑,调度是通用基础设施,它不做,因为做了反而会把你锁死在它的调度模型里。

如果非要用一个类比的话,Cron 是闹钟,Agent 是人。真实世界里你需要的是"人知道自己几点该干什么",而不是"闹钟一响,人就被迫弹起来执行任务"。前者有判断力,后者只是条件反射。

2. 给 Agent 加定时任务:三种务实方案怎么选

既然 dsh 不内置 Cron,定时任务的实现就得由我们自己搭建。这里我梳理出三种方案,它们的差别本质上是"调度逻辑放在哪一层"的问题。

2.1 方案一:外部 Cron 调度,Agent 只负责"被叫醒后干活"

这是最直接、门槛最低的方案,也是我强烈建议第一次尝试的人使用的方案。你完全不需要动 dsh 的代码,只需要在操作系统层面写好 crontab,到点调用一次 dsh 的命令行入口,让 Agent 跑一个指定任务,跑完退出即可。

# 每天 09:00 调用 dsh 执行日报任务 0 9 * * * /usr/local/bin/dsh run --task daily-report >> /var/log/dsh-cron.log 2>&1

这种方案的运行逻辑非常清晰:Cron 负责"什么时候叫",dsh 负责"被叫醒之后干什么"。两者彻底解耦,任何一个挂了都不会影响另一个。哪怕你的 dsh Agent 因为各种原因崩了没跑起来,Cron 依然准时触发,你只需要在日志里看到一次失败记录就行。

方案一的优点很突出,但也有限制。最大的限制是"单机、单次"。它适合那些流程固定、执行时间可预期、不同任务之间没有复杂依赖关系的场景。比如我最早接的"每天汇总团队日报"就是典型的方案一场景。一旦你的 Agent 任务运行时间不确定,或者需要跨多台机器做分布式调度,Cron 就不够用了。

如果一台机器上部署了多份 dsh Agent 进程,crontab 还会出现"同一个任务被多个进程同时执行"的重复问题。解决办法下文会细讲,这里先给结论:用 flock 做锁,或者给任务加全局唯一 ID,在 Agent 侧做去重。

适用人群:刚开始用 dsh 不超过一周、任务数量一只手数得过来、对调度没有高可用要求的团队。这个方案能让你用一个下午就把定时任务跑起来,而且非常稳定。

2.2 方案二:Agent 角色里内置"自我唤醒"循环,把定时做成技能

方案一的本质还是"外部的人在给 Agent 定闹钟",而方案二是把"决定何时醒来"这件事交给 Agent 自己。dsh 不是只给了三个工具吗?你可以在工具集里挂一个wait_until,让 Agent 自己决定什么时候执行下一步。

# 伪代码示意:在 Agent 技能里实现自我唤醒循环 from time import sleep from datetime import datetime, timedelta while True: now = datetime.now() next_run = now.replace(hour=9, minute=0, second=0, microsecond=0) if next_run <= now: next_run += timedelta(days=1) sleep((next_run - now).total_seconds()) agent.execute("daily_report_aggregation")

这段代码看起来很简单,但它背后代表了一种完全不同的思路:不是"闹钟到点叫醒人",而是"人自己设了一个番茄钟,到点了知道自己该切任务"。Agent 在循环中拥有完全的自主权,它可以跳过某次执行、调整执行顺序、根据上一次的结果动态决定下一次的执行时间。

方案二特别适合那种"Agent 需要长期常驻、任务之间有关联、执行时间不固定"的场景。比如一个自动巡检型 Agent,它必须先读取昨天的巡检记录,才能决定今天巡检的重点范围,那你就不能用一个写死的 Cron 时间表。它在每次循环开始前都会读取持久化的状态和上次任务结论,再生成本次的执行计划。

但这个方案也有代价。最大的问题是:进程必须常驻。如果你的服务器重启了,或者 Agent 进程因为 OOM 被杀掉,整个循环就断了。虽然 dsh 的上下文恢复机制能帮你找回记忆,但定时循环本身不会自动重启。

所以凡是使用方案二的场景,我都建议配合 Nohup、systemd 或者 Docker restart 策略一起使用,保证 Agent 进程本身具备"死了能拉起来"的能力。否则你会遇到一个很尴尬的情况:Agent 自己不知道已经漏跑了两次定时任务。

适用人群:对 Agent 自主性有要求、任务链路复杂、不满足于简单时间表驱动的开发者。这类人往往已经能接受"Agent 不是脚本机"这个前提。

2.3 方案三:把调度器封装成 dsh 插件,挂到 dsh market

方案三是最"dsh 原生"的做法:自己写一个调度工具集,封装成一个插件,发布到 dsh market。这样你在 dsh 内部就能使用schedule:list、schedule:add、schedule:run这类指令,既保留了 dsh 的统一入口,又实现了定时能力。

从热词里可以看到dsh plugin --profile web add dshmarket这类操作,说明 dsh 的插件市场是一条成熟而且被推荐的扩展路径。你把自己的调度插件挂上去之后,团队里的其他人可以通过 marketplace 直接安装复用,不需要各自造轮子。

插件的基本结构一般是一个清单文件加一组工具实现。核心伪代码如下:

# 插件入口:三个新工具的注册声明 tools = [ { "name": "schedule:add", "description": "注册一个定时任务,格式为 cron 表达式", "parameters": {"task_name": "str", "cron_expression": "str", "action": "str"} }, { "name": "schedule:list", "description": "列出当前所有已注册的定时任务", "parameters": {} }, { "name": "schedule:run", "description": "立即触发某个定时任务", "parameters": {"task_name": "str"} } ]

底层你可以用一个后台线程 + 最小堆或者直接跑 APScheduler,维护一个内存任务表。但别高兴太早,这个方案如果要做生产级,内存态是不够的。需要把任务表持久化到 SQLite 或者 Redis,才能避免 Agent 重启后任务全部丢失。

三种方案各有适用场景,我简单整理了一个横向对比,方便你在选型时快速判断:

对比维度方案一:外部 Cron方案二:Agent 自循环方案三:dsh 插件
实现成本最低,一行 crontab中等,需要常驻进程较高,需要写插件和维护任务表
自主性低,Agent 完全被动高,Agent 可动态决策中,任务注册后由调度器执行
可观测性依赖日志依赖进程状态插件内部可做状态查询
适合场景简单固定任务复杂决策链路团队级复用、多任务管理

我的建议是:别一上来就选方案三。先用方案一把最小闭环跑通,确认你的任务确实需要复杂调度之后, 再考虑提升到方案二或三。

3. 实操演示:让 dsh Agent 每 8 小时自动巡检并出报告

3.1 场景定义与任务拆解

我这次要解决的具体需求是:让 Agent 每 8 小时自动巡检一次某个内部服务的健康状态,如果发现异常,就把异常信息和初步排查建议整理成一份报告,存放在指定目录。这个任务有几个特点,注定它没办法直接用简单的 Cron 糊弄过去:巡检时间跨越凌晨、报告格式需要按模板动态生成、异常时需要 Agent 自动追加粗排分析。

8 小时这个时间周期,换算成 cron 表达式就是0 */8 * * *。很多人第一次看到这个表达式会愣一下,其实拆开就很好理解:第一个0代表分钟,这里表示整点触发;*/8代表每隔 8 小时触发一次;后面三个*分别是"日、月、周",表示不加限制。整条表达式的意思是:每天 0 点、8 点、16 点各跑一次。

这里要特别注意一个细节:Cron 表达式的*/8是按小时粒度求余的,也就是说每天的 0、8、16 点触发,而不是"从你配置的那一刻起,每 8 小时触发一次"。如果你希望严格按"从部署时间开始,每 8 小时"来跑,Cron 是做不到的,只能自行计算偏移量。

3.2 方案一的完整落地过程

我先按方案一落地一版。整体流程是:crontab 到点调用一个封装好的 shell 脚本,脚本负责拉起 dsh 命令行入口,传入固定的任务描述,Agent 执行完成之后把结果写入指定目录,同时把所有日志追加到统一日志文件。

#!/bin/bash # /usr/local/bin/dsh-health-check.sh # 该脚本负责:设置环境、调用 dsh、记录日志、保证不重复执行 exec 9>/tmp/dsh-health-check.lock if ! flock -n 9; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] 已有实例在运行,本次跳过" >> /var/log/dsh-health-check.log exit 0 fi cd /opt/dsh/workspace export PATH="/usr/local/bin:$PATH" echo "[$(date '+%Y-%m-%d %H:%M:%S')] 开始执行健康巡检" >> /var/log/dsh-health-check.log dsh run --task "请检查 http://localhost:8080/healthz 接口是否正常,如果返回码非 200 或响应时间超过 500ms,请检查服务日志并给出初步排查建议。输出一份 Markdown 格式的健康报告,保存到 ./reports/health-$(date +%Y%m%d-%H%M).md" echo "[$(date '+%Y-%m-%d %H:%M:%S')] 执行完毕" >> /var/log/dsh-health-check.log

然后在 crontab 里注册任务:

# 写入 crontab -e 0 */8 * * * /usr/local/bin/dsh-health-check.sh >> /var/log/dsh-health-check.log 2>&1

代码里的第一个关键点是 flock 文件锁。多人维护的服务器上最容易出现的问题就是:运维同学手动跑了一次巡检脚本,cron 到点又自动跑了一次,两个进程同时执行,互相覆盖报告。加了 flock 之后,谁先抢到锁谁执行,另一个直接退出,抑制了重复执行的风险。

第二个关键点是cd /opt/dsh/workspace。Cron 环境是一个"干净环境",它不会继承你在终端里设定的环境变量和当前目录。如果不先cd到工作目录,dsh 可能会因为找不到配置文件或者相对路径而启动失败。这算是定时任务天然带的一个坑。

第三个关键点是日志重定向。Agent 执行过程能跑很久,如果没有重定向,很容易出现控制台输出丢失。统一>> /var/log/dsh-health-check.log 2>&1,无论是正常日志还是错误日志,都能在同一份文件里追踪。

3.3 方案一跑起来之后我观察到的问题

我第一次跑通之后,整个系统确实稳定运行了两天,但第三天的报告出现了问题:凌晨 0 点那次的 Report 内容和前一天 16 点的一模一样。排查后发现,服务在半夜其实已经返回 500 了,但 Agent 执行的 prompt 里没有提示"如果上次报告已经存在,请对比一下前后差异",所以它直接覆盖写了一份看似正常、实际毫无新信息的报告。

这个问题暴露出方案一的一个深层缺陷:外部 Cron 只负责触发,但任务的具体执行质量完全依赖 prompt 的质量。如果是人在电脑前用交互式 Terminal 使用 Agent,你可以在看到错误输出后立刻追加指令,让 Agent 重新分析。但定时任务里没人盯着,prompt 写得不严谨,Agent 就会按它理解的"正常执行"来完成。

修正方式是在 prompt 里加上明确指令:先检查目标报告目录中最近一次报告的时间戳和内容摘要,如果本次巡检的状态码或响应耗时与上次差异超过阈值,优先强调差异并检查这段时间内是否有变更记录。这种"把任务逻辑显式写进 prompt"的习惯,是所有定时 Agent 任务里最重要的一环。

3.4 如何升级到方案二的实现片段

方案一验证了任务本身的正确性之后,我决定升级到方案二,让 Agent 拥有自主巡检能力。核心思路是给 Agent 一个"等待"工具,然后在一个常驻进程中运行它。

# keep_alive.py # 一个极简常驻入口,每 8 小时唤醒一次 Agent import time import subprocess from datetime import datetime INTERVAL_SECONDS = 8 * 60 * 60 def run_agent(): result = subprocess.run( ["dsh", "run", "--task", "健康巡检与报告生成"], capture_output=True, text=True, cwd="/opt/dsh/workspace" ) with open("/var/log/dsh-self-loop.log", "a") as f: f.write(f"[{datetime.now().isoformat()}] exit={result.returncode}\n") f.write(result.stdout[-2000:]) f.write("\n") def main(): while True: run_agent() time.sleep(INTERVAL_SECONDS) if __name__ == "__main__": main()

这段代码比 Cron 多出来的价值在于:Agent 进程本身是常驻的,这意味着它可以保留上下文和内部状态。dsh 本身有 Agent 记忆能力,你可以在任务描述中让它参考"上次巡检时记录的问题列表",这样 Agent 会优先检查之前标记过"重点观察"的指标。

方案二有个残酷的现实要求:进程不能被随意杀掉。我会用 systemd 去做守护:

# /etc/systemd/system/dsh-health-agent.service [Unit] Description=dsh 健康巡检常驻 Agent After=network.target [Service] ExecStart=/usr/bin/python3 /opt/dsh/keep_alive.py Restart=always RestartSec=30 [Install] WantedBy=multi-user.target

配置好之后执行systemctl enable --now dsh-health-agent,即使进程意外崩溃,systemd 也会在 30 秒后拉起来继续跑。这样既保住了 Agent 的自主决策能力,又解决了常驻进程的可靠性问题。

4. 跑起来之后才知道的坑:常见问题与排查技巧

4.1 同一个 Cron,在 Docker 里的行为完全不同

很多团队会把 dsh 跑在 Docker 容器里,我遇到过最经典的坑是:宿主机上date显示的时区是 CST 北京时间,但容器里默认是 UTC 时间。结果 Cron 设定的 8 点,实际执行却是北京时间 16 点。整整晚了大半天,没有人发现。

排查方法很简单,进容器执行date,如果不是你预期的时区,就看看/etc/localtime是否正确。但更稳妥的做法是:在 dsh 的任务 prompt 中显式携带当前时间。你在外层传递的不是"按 Cron 时间触发",而是"当前时间是 2025-06-15 08:00:00(UTC+8),请据此决定巡检时机"。

这招很受用,因为我们真正需要 Agent 感知的不是"机器时钟",而是"业务时钟"。容器里跑什么时区,并不影响 Agent 对北京时间的判断,只要 prompt 里给出了权威时间源就行。

4.2 任务重复执行与任务静默丢失

这两个问题往往同时出现,但原因完全不同。重复执行通常出现在多机部署场景:同一个 crontab 同步到了两台服务器上,两台机器的 Agent 同时开始执行任务,生成了两份内容几乎一致的报告,后写的覆盖了先写的。我之前的 flock 方案只能解决单机上的重复,解决不了跨机器的重复。

跨机器的正确解法是引入一个全局分布式锁或者一个"任务执行记录表"。比如在 Redis 里维护一个 key,键名是任务的唯一 ID,值设为1,过期时间设为任务预计执行的超时时间。各机器的 Agent 在执行前先尝试SET key 1 NX EX 300,只有返回成功的那台机器才真正执行任务。这个套路在 xxljob、Spring 分布式定时任务里也非常相似,本质都是分布式互斥。

任务静默丢失则是另一类问题,常见于常驻进程被 OOM Killer 杀掉之后,内存队列里的所有定时任务全部归零,而你只靠 log 文件根本看不出来。解决方案很简单:把所有任务定义持久化到 SQLite 或 Redis,进程启动时先恢复任务表,再开始调度。内存调度器可以挂,但任务元数据不能挂。

4.3 5 分钟快速定位问题的排查三步法

定时任务的问题,最麻烦的就是"你不知道它该不该在这时候跑"。我摸索了一套极简排查法,遇到任何异常直接按三步走:

第一步,确认 Agent 有没有醒。查看日志文件里有执行开始标记的时间戳,如果在预期执行时间点没有记录,多半是触发层的问题,要么 Cron 没生效,要么 systemd 服务挂了。第二步,确认任务有没有进队列。如果是方案二或三,就去看看调度器维护的任务状态表,有没有 Pending、Running、Done 的记录。第三步,确认执行结果有没有回传。如果有开始标记但没有结束标记,多半是 Agent 执行过程中卡住了,这时候就去看 Agent 的详细日志输出。

这三步看起来简单,但真正做到位需要日志规范。我强烈建议在封装脚本的每一层都输出统一格式的时间戳和状态码,比如[INFO] 2025-06-15 08:00:01 task_started,否则排查时你会在不同日志文件里来回翻,浪费时间。

下面是我整理的速查表,遇到问题直接对照:

症状可能原因处理方式
到了时间没执行Cron 未生效 / 时区不对检查crontab -l、容器时区
执行了但没输出PATH 环境变量缺失脚本开头 export PATH
结果和上次一模一样prompt 缺少差异对比指令重写任务描述
同一任务两周内没跑进程被杀 / 循环断掉检查 systemd、进程 PID
任务并发重复执行多机部署未加锁引入 Redis 分布式锁
Agent 中途卡死工具调用超时在 prompt 设置超时指令

4.4 并发上来之后怎么扛

"AI Agent 怎么扛并发"是一个高频热搜问题。放在定时任务这个语境下,它其实分两种情况:一是多个不同的定时任务同时触发,二是同一个任务同时启动多个实例。

多个不同任务同时触发,在单机上直接用进程隔离就好,dsh 本身处理并发并不需要额外加锁。真正麻烦的是第二种,所以我在方案一的脚本里用了 flock,在方案三的插件设计里用了分布式锁,目的都是确保"同一时刻一个业务动作只有一个执行者"。

换一种更通用的说法:如果你的任务执行时间远大于调度间隔,那么排队和并发控制就不可避免。比如一个任务要跑 30 分钟,而调度间隔是 10 分钟,那必然堆积。此时要么把任务拆小,要么引入一个信号量来限制最大执行实例数。我个人经验是,先用简单的进程锁顶住流量,等真的出现并发瓶颈了,再升级到分布式队列也不迟。别在只有三个定时任务的阶段,就直接上 Celery 或者 xxljob,那是给自己添乱。

5. 进阶方向:把调度插件做成 dsh market 里的一个正式组件

5.1 dsh 插件的基本挂载方式

dsh 的插件机制是我用过的最省心的 Agent 扩展方式之一。e从热词里可以看到类似dsh plugin --profile web add dshmarket的命令,这就是通过命令行把 marketplace 插件源挂到当前 profile 上的操作。挂载之后,你可以直接安装、更新别人共享的插件工具集。

一个插件的核心是一份清单文件,声明插件名、版本、作者和提供的工具列表,然后用一个 Python 或 Node 模块实现具体逻辑。dsh 在 Agent 决策时会自动扫描已安装插件的工具描述,大模型发现当前任务需要"定时调度"时,就会选择调用schedule:add这类自定义工具。

5.2 我们的定时任务插件可以再升级什么

如果只是想给自己用,方案一或方案二完全够了。但如果你打算把插件发布到 dsh market 上,那有几个细节值得打磨:第一,任务表必须持久化,避免 Agent 重启后忘记所有已注册任务;第二,要有死信队列,任务连续失败后不能无限重试,要有人工介入的出口;第三,提供简单的 Webhook 通知能力,任务完成或失败时推送到钉钉或企业微信。

不过我也要提醒一声:调度能力做到"够用"就刹车。你越是往定时机制里堆功能,Agent 的自主空间就越小。我在设计插件时给自己定了一个原则:定时触发的动作只负责"把 Agent 叫醒并给它一个任务点",至于任务怎么拆解、执行到什么程度、要不要跳过,全部留给 Agent 自己去判断。这条边界守不住的话,这个插件就变成了一个披着 Agent 名字的定时脚本平台。

从我个人的实际感受来说,dsh 故意不内置 Cron,反而是让我把更多精力放在了真正该思考的地方:我的 Agent 到底应该被唤醒后做什么。定时触发只是最外层的一个哨兵,哨兵不需要成为主角。这个取舍,值得每个在做 Agent 开发的人认真琢磨。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询