定时任务还想上 Hangfire?这个被 AI Agent 项目看上的 TickerQ,把反射全干掉了
写后端的人都躲不开定时任务:每天凌晨跑对账、每五分钟拉一次订单状态、给超时未支付的订单自动关单。.NET 这边的选择说多也多,说少也少——Quartz.NET 功能全但配置啰嗦,Hangfire 用着顺手,可一上 Dashboard 就开始掏钱,而且它那套基于反射的作业发现,在 Native AOT 时代多少有点格格不入。
今天要聊的 TickerQ,走的是另一条路:编译期用 Source Generator 把所有作业注册好,运行时零反射,自带持久化和监控面板。GitHub 上 3600 多个 star,最近 .NET 圈子里讨论度不低。
不过这篇不只是把别人介绍过的东西再抄一遍。真正让我注意到它的,是 NuGet 官方页面上一个细节:在"依赖 TickerQ 的热门仓库"榜单里,除了 ABP 这种企业级框架,还躺着 clawdotnet/openclaw.net——一个用 .NET 写的自托管 AI Agent 运行时。一个把 Native AOT 写进标语的项目,定时调度这块交给了一个"新星"库。顺着挖下去,挖到一个挺有意思的工程案例:用 TickerQ 给 AI Agent 装"定时心跳",实现 /loop 定时循环指令。

TickerQ 是什么
仓库: https://github.com/Arcenox-co/TickerQ ,文档: https://tickerq.net 。
先报数据,2026 年 9 月下旬从 GitHub API 拉的:3,600+ star,210+ fork,仓库 2023 年 4 月创建,更新很勤。License 是 MIT + Apache 2.0 双协议,商用没有障碍。版本号从 2.x 直接跳到 10.x,团队解释是为了跟 NuGet 包版本对齐,看着吓人,其实就是数字游戏。
传统调度框架找作业的方式大同小异:启动时扫程序集,用反射找带特定 Attribute 的方法,再靠字符串配置把它们串起来。这套玩法有两个毛病:启动慢、有开销是一方面;更要命的是方法名、cron 表达式全写在字符串里,写错了到运行时才炸。而且反射和 AOT 裁剪天生犯冲,.NET 8 之后想把服务发布成 Native AOT,Hangfire 这类框架基本没戏。
TickerQ 的做法是把这件事挪到编译期。你写一个普通的类方法,打上 [TickerFunction] 特性,Source Generator 在编译时就把方法名、类型、调度配置全部生成好。换来三个好处:启动没有反射扫描的开销;cron 写错了编译期直接报错;整个库可裁剪、可 AOT,官方首页明晃晃写着 Native AOT ready。
核心包就一个,dotnet add package TickerQ 装完,三步走:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddTickerQ();
var app = builder.Build();
app.UseTickerQ();
app.Run();
using TickerQ.Utilities.Base;
public class MyJobs
{
[TickerFunction("HelloWorld")]
public async Task HelloWorld(
TickerFunctionContext context,
CancellationToken cancellationToken)
{
Console.WriteLine($"Hello from TickerQ! Job ID: {context.Id}");
}
}
定时跑就用 cron 特性标注:
[TickerFunction("Cleanup", cronExpression: "0 2 * * *")] // 每天凌晨两点
public async Task Cleanup(TickerFunctionContext ctx, CancellationToken ct)
{
// 清理过期数据...
}
想临时插个一次性任务,注入 ITimeTickerManager<TimeTickerEntity>,AddAsync 一调就完事。
几个我在意的设计:
持久化用你自己的库,不另起炉灶。EF Core Provider 支持 PostgreSQL、SQL Server、SQLite、MySQL,不想引 EF 还有 Redis Provider。作业状态就躺在你自己的基础设施里,备份、迁移都顺手。
Dashboard 不收钱。基于 SignalR 的实时面板,执行状态、失败记录、重试情况都看得见,还能直接在面板上启用/禁用某个 cron 作业,不用改代码不用重新发布。这个功能在生产环境是真的救命。
多节点也做了。加实例,靠 Redis 心跳做分布式协调,节点挂了有 dead-node 清理,锁机制保证同一个作业不会被两个节点同时抢走。
OpenClaw.NET 又是什么
https://github.com/clawdotnet/openclaw.net ,一个自托管的 AI Agent 运行时。跑一个本地网关,连托管或本地模型(OpenAI、Claude、Gemini、DeepSeek、Ollama 都有),给 Agent 配 80 多个工具面和记忆,再通过桌面端(Avalonia 写的 AgentQi Companion)、浏览器、CLI、TG、WhatsApp 这些渠道对话。
它的工程取向很明确:Native AOT 优先,启动快,内存低,零反射。发行包直接附带 AOT 编译好的网关和 CLI,解压就能跑。
这就是理解它选型 TickerQ 的钥匙。一个把 Native AOT 当命根子的系统,挑调度框架时,第一眼就会把所有反射重、裁剪不友好的老面孔筛掉。
用 TickerQ 给 AI Agent 装"定时心跳"
OpenClaw.NET 最近合并的 /loop 命令(PR #156,社区贡献者 geffzhang 提交,思路来自 OpenAI Codex 的 /loop 原语),是个不错的 TickerQ 实战样本:1,033 行代码,11 个文件,62 个测试。
功能不复杂:聊天界面输入 /loop 5m check CI status,系统每 5 分钟往会话里注一次系统消息,驱动 Agent 持续检查 CI 状态并汇报,直到任务完成自动终止。拆开来,里面有几个地方做得挺讲究。
混合调度。 这是整个实现的地基。TickerQ 当前的版本有个特点:cron 表达式只支持编译期声明,不暴露"运行时再动态建一个每 5 分钟跑的任务"这种接口。你只能写死 [TickerFunction("AgentLoopExecutor", "* * * * *")],编译完就定了。但 /loop 显然要动态调度——用户随时发起新循环,间隔还各不相同。
PR 的做法是:编译期挂一个每分钟 tick 一次的 TickerQ 定时器当"心跳",运行时维护一张 ConcurrentDictionary<string, LoopEntry> 内存注册表,记每个活跃 loop 的下次触发时间和间隔。心跳一到,扫一遍注册表,把到期的推进一轮。
没魔改 TickerQ,也没引入第二个调度依赖,ConcurrentDictionary 的线程安全性久经验证。代价是多了一层内存轮询,但对分钟级精度完全够用。这条经验对 TickerQ 用户挺值钱:框架不给动态 cron,不代表做不了动态调度,一个高频心跳加一张注册表,就把"编译期调度器"和"运行时动态任务"缝在了一起。
零入侵 AgentRuntime。 Loop 的提示词不是直接塞给模型,而是通过 MessagePipeline.InboundWriter 以 ChannelId="cron" 的系统消息注入。这条消息走和正常用户消息完全相同的投递管道,由已有的 GatewayInboundMessageWorker 消费、转发。AgentRuntime 一行代码没改——MafAgentRuntime 也一样。
这里补一句背景:OpenClaw.NET 是双运行时结构。除了内置的 AgentRuntime,还有一个基于 Microsoft Agent Framework 的 MafAgentRuntime,两个运行时实现同一套入站消息接口。/loop 的消息走的是管道,不碰任何一个运行时的内部实现,所以对两个运行时等效生效。整个 PR 里我最喜欢的就是这个决定。
双层终止。 这块做得有点 paranoid,但事出有因。OpenAI Codex 早期曾因缺乏有效终止机制,Agent 干完活继续空转,日志膨胀到 34GB/天。所以 PR 用了两层兜底:主路径是模型主动调 loop_control(status="complete") 工具,显式声明干完了;备用路径是检测响应文本里的终止关键词(LOOP_TERMINATE、DONE、WORK_COMPLETE 之类),用 FrozenSet 存,O(1) 查找。只有被无限循环整过的人,才会在终止条件上这么较真。
零反射。 整个实现零反射、零动态代码生成:[GeneratedRegex] 编译期生成正则,[TickerFunction] 源生成器处理调度声明,System.Text.Json 源生成器序列化 payload,关键词集合用 FrozenSet 冻结。Native AOT 会把反射和动态代码裁掉,轻则性能掉,重则直接起不来。TickerQ 的零反射在这里不是性能卖点,是整套方案能编进 AOT 二进制的前提。
调度之外还有一张精简的三态机:Scheduled → Running → Terminated(终态)。配套三条规则:同一个 session 只能有一个活跃 loop,重复发起直接覆盖旧的;上一轮没跑完时新消息排队等,不强行中断;语义终止后不弹通知,安静退出,把界面还给用户。最后这条我觉得是细节见功力——没人愿意被一个已经没必要的循环反复弹窗。
/loop 还跟 OpenClaw.NET 里另一个原语 /goal 形成互补:/goal 是模型一停就自动续跑,适合"修完全部测试"这种一口气推进的;/loop 是定时巡检,适合"每 5 分钟看一眼 CI"这种等外部状态变化的。同一个 session 可以同时挂俩,一个巡检,一个推进。
为什么这对 TickerQ 算个背书
一个把 Native AOT 当命根子的系统,把定时调度的地基交给 TickerQ,逻辑很清楚:零反射加源生成,能编进 AOT 二进制,这是硬门槛;cron 编译期校验,心跳表达式写错构建直接失败;核心包零外部依赖,符合运行时"克制"的切分哲学;Dashboard 和持久化按需引入,用多少引多少。
ABP 用 TickerQ,证明它能在企业级框架里站住脚。OpenClaw.NET 这个案例证明的是另一件事:在 AOT、源生成这条新技术路线上,它是目前 .NET 里比较顺手的调度原语。
这个案例顺带说明的事
今年 AI 圈概念迭代快得应接不暇:Prompt Engineering 还没消化,Context Engineering 登场;Harness Engineering 刚存进收藏夹,Loop Engineering 又来了。判断新概念是不是噱头,最好别争论定义,去看真实的工程实现。
/loop 的本质可以压成一句话:Loop = Cron(定时触发)+ 决策器(判断下一步干什么)。Claude Code 的负责人今年说过一句类似"我不再 prompt Claude 了,我写 loop 让它们运行"的话。人的活计正在从"跟模型对话"变成"设计模型的运行规则"。而"定时触发"这一半,TickerQ 这类现代调度器已经把路铺好了。
想动手的话,建议先问自己五个问题:触发条件清楚吗?上下文可信吗?工具边界受控吗?结果可验证吗?停止条件明确吗?都有答案再写 loop,缺一个,就是把混乱自动化。
泼点冷水
说点缺点,免得你以为我在恰饭。
第一,TickerQ 还年轻。3600 star 在 .NET 生态里只能算新星,对比 Hangfire 十几年生产验证,坑肯定没踩完。GitHub 上挂着 100 多个 open issue,用之前先搜一下自己的场景是不是已知问题。
第二,文档还在追赶代码。部分高级用法得翻源码示例确认,不如老牌框架厚实。
第三,/loop 的动态调度是靠内存注册表实现的——进程一重启,活跃 loop 就没了。对"每 5 分钟看 CI"这种可重建的循环无所谓,但如果你要做重启后必须恢复的动态任务,得自己接 EF Core 持久化做恢复逻辑,动手前先评估清楚。
第四,场景简单就别上框架。一个后台服务里跑个 PeriodicTimer 就够的活,别为这个引库。杀鸡别用牛刀。
适合谁:被 Hangfire 配置或收费困扰的团队;要上 Native AOT 的项目;需要免费监控面板和多节点协调的中小型系统;还有正在做 AI Agent、需要给 Agent 装"定时心跳"的人。不适合谁:极度求稳、非十年老库不用的团队;只有两三个定时任务的玩具项目。
结语
折腾下来的感觉是:TickerQ 踩中了 .NET 转向 AOT 和源生成的大趋势,设计在线;OpenClaw.NET 的 /loop 则把这个趋势落到了一个看得见的场景里——给 AI Agent 写定时循环这件事,用 .NET 的源生成调度器一千行左右就能落地,还全程 AOT。
当不了 Hangfire 的平替,但完全可以进"下一个项目默认选它"的候选名单。哪天你的 Agent 需要一颗定时心脏,说不定会想起这个组合。
参考链接:
- TickerQ 仓库:https://github.com/Arcenox-co/TickerQ
- TickerQ 官方文档:https://tickerq.net
- OpenClaw.NET 仓库:https://github.com/clawdotnet/openclaw.net
- OpenClaw.NET PR #156(/loop 实现):https://github.com/clawdotnet/openclaw.net/pull/156
欢迎大家扫描下面二维码成为我的客户,扶你上云

浙公网安备 33010602011771号