Agent 如何持续工作:任务持久化、后台执行与定时唤醒

从零实现 Agent Harness 系列 · 第 06 篇

这一篇讲三个能力:Persistent TasksBackground TasksCron Scheduler

它们分别回答三个问题:任务状态怎么保存,长动作怎么异步执行,未来任务怎么定时回来。

前言

一个最小 Agent 很适合处理短任务。

比如:

text 复制代码
读一个文件
改一个函数
跑一次测试
解释一下结果

这类事情通常可以在一次对话、一次主循环里做完。

但真实工作不是这样的。

真实工作经常会拖得更久,也更像一个过程:

  • 完整测试套件可能要跑很久
  • 某个问题今天先记下来,明天还要继续查
  • 构建、扫描、部署这类动作不适合一直占着主循环
  • 有些检查需要每天、每周、每隔一段时间自动执行
  • 用户下次回来时,希望 Agent 还能知道上次做到哪里

这时,单纯依赖"当前对话 + 当前上下文"就不够了。

长期运行至少意味着三件事:

text 复制代码
状态不能只活在 messages 里
长动作不能一直阻塞 agent loop
未来工作要能自动回到主循环

这一篇要讲的三层机制,正好分别对应这三个核心问题:

text 复制代码
Persistent Tasks:任务状态如何持久化
Background Tasks:长时间动作如何异步执行
Cron Scheduler:未来任务如何定时触发

它们合在一起,才让 Agent 从"即时问答工具"开始变成"可以持续推进工作的系统"。

一、为什么 Agent 需要长期运行能力

前面几篇已经解决了不少基础问题:

  • Agent Loop 让模型能调用工具
  • 临时步骤记录让模型能管理当前任务进度
  • Subagent 让探索过程隔离出去
  • Skill Loading 让模型按需加载专业知识
  • Context Compact 让长上下文可以被压缩

这些能力已经能让 Agent 做出不少像样的事情,但它们主要还是围绕"当前会话"展开的。

只要任务跨越时间,问题就会立刻冒出来。

1. 任务状态会丢

如果任务只写在 messages 里,它就不是稳定状态。

因为对话历史本质上是"给模型看的上下文",不是"给系统保存的状态库"。

这两者看起来很像,但职责完全不同:

  • 上下文的目标是让模型理解当前情况
  • 状态的目标是让系统在未来还能恢复工作

一旦把这两种职责混在一起,就会出现很多问题:

  • 进程退出后,内存里的东西可能直接丢失
  • 上下文压缩时,重要细节可能被摘要掉
  • 多个任务之间的依赖关系很难维护
  • 用户隔一天再回来时,Agent 可能只记得"大概做过什么",却不知道精确进度

所以长期任务不能只靠聊天记录保存。

2. 长命令会卡住主循环

普通工具调用通常是同步的:

text 复制代码
模型调用工具
程序执行命令
等待命令结束
结果返回模型

这对短命令没问题,比如 pwdrgpython -m py_compile

但如果是:

  • 完整测试
  • 构建产物
  • 安装依赖
  • 启动服务
  • 扫描大仓库
  • 跑数据处理脚本

主循环就会一直卡在"等待命令结束"这一格。

这时最麻烦的地方不是"慢",而是"慢的时候 Agent 什么都做不了"。

它不能继续规划,不能顺手处理其他独立任务,也不能先整理已经拿到的信息。

3. 未来事件无法自动回来

还有一些任务根本不是"现在就做",而是"未来做"。

比如:

  • 每天早上检查一次项目状态
  • 每 10 分钟查看一次后台任务
  • 每周生成一次工作摘要
  • 明天继续跟进某个未完成问题

如果没有调度机制,Agent 只能等用户再次提醒它。

这说明长期运行的核心,不是"让一个 loop 永远不退出",而是建立三条能力链路:

text 复制代码
能保存状态
能异步等待
能被未来事件唤醒

4. 这三件事为什么要拆开做

很多人刚开始会直觉地想:

"能不能做一个更大的 Agent Loop,把这些问题一次性都包进去?"

当然可以,但那样很快会把不同职责揉成一团。

因为这三类问题其实属于三个不同层次:

  • Persistent Tasks 管的是"状态"
  • Background Tasks 管的是"执行通道"
  • Cron Scheduler 管的是"触发时机"

把它们拆开有两个好处。

第一,概念清楚。你会知道每一层到底在解决什么,不会把"任务存储""后台执行""定时唤醒"混成一个模糊的大功能。

第二,实现更稳。状态层可以单独升级成数据库,后台执行层可以单独升级成 worker 队列,调度层也可以单独接 webhook 或外部事件,不会互相绞在一起。

所以这篇文章虽然讲的是三个机制,但本质上是在搭一条"长期工作能力链"。

二、Persistent Tasks:让任务活在对话之外

第一层是 Persistent Tasks

它解决的是一个很基础、但很关键的问题:

text 复制代码
任务状态应该放在哪里?

对当前请求来说,用一份临时步骤清单来跟踪进度已经够用了。

但这类状态通常只存在于当前运行过程中。

一旦任务要跨轮、跨会话继续推进,就需要把状态写到对话外部。

1. 原理:把"上下文里的计划"变成"系统里的记录"

这里最核心的思想并不复杂:

任务不应该只是模型脑子里的一串待办项,而应该变成一份系统能读、能写、能恢复的结构化记录。

只要任务被结构化保存下来,系统就能做很多对话本身做不到的事情:

  • 精确恢复某个任务的状态
  • 查询有哪些任务还没完成
  • 表达任务之间的依赖关系
  • 在将来某个时间点继续推进同一个任务

换句话说,Persistent Tasks 不是让 Agent "记得更多",而是让 Agent 有一个对话之外的状态层。

2. 这一份实现怎么做

在这份实现里,这一层有两个核心部件:

  • .tasks/:保存任务 JSON 文件的目录
  • TaskManager:负责创建、读取、更新任务,并维护依赖关系

一个任务大致长这样:

json 复制代码
{
  "id": 3,
  "subject": "修复支付回调重试",
  "description": "排查失败原因并补回归测试",
  "status": "pending",
  "blockedBy": [2],
  "owner": ""
}

这里最关键的是几个字段:

  • id:任务唯一标识,方便查询和建立依赖
  • subject:任务标题,让模型和人都知道这是什么
  • description:更完整的说明,避免标题过短导致语义不够
  • status:当前状态,比如 pendingin_progresscompleted
  • blockedBy:当前任务被哪些上游任务阻塞

这一步的意义,是把"聊天里的任务清单"升级成"可恢复、可查询、可维护依赖关系的任务系统"。

3. 从工具调用到文件落盘

模型如果调用:

text 复制代码
task_create(subject, description)

程序不会只在对话里多留一句"我记住了",而是会真的写入:

text 复制代码
.tasks/task_<id>.json

后续模型可以再通过:

text 复制代码
task_list
task_get
task_update

读取和更新这些任务。

这意味着任务状态第一次从"语言里的承诺"变成了"系统里的事实"。

只要文件还在,哪怕:

  • 这轮对话结束了
  • 上下文被压缩了
  • 进程重启了

任务也依然存在。

4. 依赖关系为什么重要

如果任务只有"创建"和"完成"两个动作,它最多只是个记事本。

真正让它更像任务系统的,是依赖关系。

比如:

text 复制代码
task 1:定位支付回调失败原因
task 2:修复重试逻辑
task 3:补回归测试

其中 task 2 依赖 task 1,task 3 依赖 task 2。

那么系统里就可以表示成:

text 复制代码
task 2 blockedBy [1]
task 3 blockedBy [2]

这样一来,Agent 不只是"知道有三件事要做",而是"知道这三件事该按什么顺序推进"。

对长期任务来说,这一点非常重要。

因为一旦工作跨度变长,真正难的往往不是"记住有哪些事",而是"记住哪些事现在可以做,哪些事还得等上游完成"。

5. 完成任务时,为什么要自动清依赖

这份实现里还有一个细节很重要:

当一个任务被标记为 completed 时,TaskManager 会自动清理其他任务里的依赖。

比如原来是:

text 复制代码
task 2 blockedBy [1]
task 3 blockedBy [2]

当 task 1 完成后,task 2 会变成:

text 复制代码
task 2 blockedBy []

这说明 task 2 不再被 task 1 阻塞,可以继续推进。

这个动作看起来只是一个小细节,但它其实是在维护任务图的一致性。

如果没有这一步,系统就会出现一种很常见的问题:

  • 上游任务已经完成
  • 下游任务理论上可以开始
  • 但状态里仍然写着"被阻塞"

于是 Agent 看到的是一份过时状态,后面的自动化能力也会跟着失真。

6. 一个具体例子

假设用户说:

text 复制代码
帮我排查支付回调失败问题,这件事今天先记着,明天继续。

如果没有持久化任务,Agent 最多只能在当前上下文里记一笔。

但有了 Persistent Tasks,它可以把这件事明确登记成一个长期任务:

text 复制代码
subject: 修复支付回调失败
description: 先定位失败原因,再修复重试逻辑并补测试
status: pending

接下来即使用户离开,第二天再回来,系统仍然能通过 task_listtask_get 找到这件事。

这就是"任务真的活在对话之外"的意思。

7. 必要说明:这一层解决什么,不解决什么

Persistent Tasks 解决的是"任务状态如何长期存在"。

它不直接解决两件事:

  • 它不负责执行耗时动作
  • 它不负责决定未来什么时候触发

所以它更像一个底座。

没有它,长期任务很容易变成一团模糊记忆。

有了它,后面的后台执行和定时调度才有地方落脚。

三、Background Tasks:让长动作不阻塞

任务状态能存下来以后,第二个问题是:

text 复制代码
耗时动作应该怎么跑?

很多动作本身就很慢:

  • 跑完整测试
  • 执行构建
  • 安装依赖
  • 启动服务
  • 扫描大仓库
  • 跑数据处理脚本

如果还像最小 Agent Loop 那样同步执行,主循环就会一直等待。

1. 原理:异步的重点不是更快,而是不阻塞

很多人提到后台任务时,第一反应是"为了并行"。

但对 Agent 来说,更本质的价值通常不是更快,而是不阻塞。

因为主循环被卡住时,损失的不只是几秒钟时间,而是整个决策链条被迫暂停:

  • 模型不能继续分析已知信息
  • 不能先处理其他独立任务
  • 不能把等待中的动作和已经完成的动作分开管理

所以 Background Tasks 的目标不是把所有事情都并行化,而是把"需要等很久"的事情从主循环里挪出去。

2. 这一份实现怎么做

Background Tasks 的思路是:

让长命令在后台执行,主 Agent 先拿到一个 task_id,然后继续推进其他工作。等后台任务完成后,再把结果带回上下文。

在这份实现里,核心部件是 BackgroundManager,里面主要有三样东西:

  • tasks:保存后台任务状态
  • _notification_queue:保存已经完成、等待交付的结果
  • _lock:保证并发读写时状态一致

一个后台任务的状态大致像这样:

json 复制代码
{
  "status": "running",
  "result": null,
  "command": "pytest tests/test_checkout.py"
}

也就是说,后台层不仅要"启动线程",还要维护一份独立状态表。

3. 一次后台执行的完整链路

一次典型流程是这样的:

text 复制代码
模型调用 background_run
    ↓
程序启动后台线程
    ↓
立即返回 task_id
    ↓
Agent 继续处理其他工作
    ↓
后台任务完成后写入通知队列
    ↓
下一次调用模型前,把结果注入 messages

比如模型调用:

text 复制代码
background_run("pytest tests/test_checkout.py")

程序不会等 pytest 跑完,而是立刻返回:

text 复制代码
Background task a1b2c3d4 started: pytest tests/test_checkout.py

这时主循环没有被卡住。模型知道测试已经在跑,就可以先去读相关代码、检查最近修改,或者处理另一个独立任务。

4. 为什么完成结果不直接打断当前对话

这里有个很容易被忽略的设计点:

后台任务完成后,结果不是立刻"闯进来"打断当前执行,而是先进入通知队列。

下一轮真正请求模型前,主循环会调用类似:

text 复制代码
inject_background_notifications(messages)

把结果整理成一条普通消息:

text 复制代码
<background-results>
[bg:a1b2c3d4] completed pytest tests/test_checkout.py: 12 passed, 1 failed
</background-results>

这么做的原因很重要。

因为主循环里的 messages、工具调用结果、上下文注入,本来就是一条相对线性的工作流。

如果后台线程一完成就直接并发改上下文,系统很容易出现几类问题:

  • 结果插入时机混乱
  • 当前对话状态被突然打断
  • 多个后台任务同时返回时互相打架

所以更稳的做法是:

text 复制代码
先完成
先入队
再在安全时机交给主循环

5. 这层真正重要的是"回流链路"

很多时候我们会把后台任务理解成"开了一个线程"。

但线程只是最表面的实现细节。

这一层真正重要的不是"开线程"本身,而是完整的回流链路:

text 复制代码
异步启动
状态可查
完成后入队
下一轮回到上下文

少任何一环,体验都会差很多。

比如:

  • 只有异步启动,没有状态查询,模型就不知道任务跑到哪了
  • 只有状态查询,没有结果回流,模型就得反复主动轮询
  • 只有结果回流,没有独立状态表,系统就难以管理多个后台任务

所以 Background Tasks 的关键,不是"后台运行",而是"后台运行之后还能重新接回主循环"。

6. 一个具体例子

假设 Agent 正在排查一个测试失败问题。

它可以一边:

  • 启动 pytest
  • 读取相关模块代码
  • 查看最近改动
  • 整理可能的故障点

等测试跑完以后,再根据返回结果决定下一步。

这时 Agent 的工作方式就从:

text 复制代码
启动测试
傻等
拿到结果
再思考

变成了:

text 复制代码
启动测试
继续推进别的工作
结果回来后再整合决策

这个差别,正是"同步工具调用"和"长期工作系统"的分水岭之一。

7. 必要说明:这一层的边界和限制

这份教学实现已经把主线跑通了,但它还是一个最小版本。

它目前主要演示的是:

  • 如何把长命令放到后台
  • 如何维护后台状态
  • 如何把完成结果重新交给主循环

它还没有覆盖很多生产级问题,比如:

  • 取消任务
  • 重试任务
  • 更细粒度的失败原因
  • worker 隔离
  • 日志持久化
  • 并发配额和优先级

所以这一层最好理解成:它先把"主循环不再被长动作卡死"这件事建立起来。

真正的工业化版本,通常会进一步演化成 job queue 和 worker 系统。

四、Cron Scheduler:让 Agent 在未来自己醒来

Persistent Tasks 解决状态保存。

Background Tasks 解决当前已经开始的长动作。

第三层 Cron Scheduler 解决的是另一个维度:

text 复制代码
未来某个时间点,怎么自动把任务重新交给 Agent?

比如用户说:

text 复制代码
每周一早上 9 点检查一次项目状态,并汇总未完成任务

这不是一个"现在立刻执行"的任务,而是一条未来规则。

1. 原理:把"未来发生什么"从当前对话里解耦出来

没有调度器时,系统通常只有两种触发方式:

  • 用户当前发来一条消息
  • 程序当前正在执行一条链路

但未来任务不是这样。

它的特点是:

  • 现在先注册
  • 到点再触发
  • 触发后重新交回主循环

所以调度层的核心职责不是"替模型思考",而是"在合适的时候重新制造一次输入"。

换句话说,调度器本身不是业务执行者,它只是未来事件的搬运工。

2. 这一份实现怎么做

在这份实现里,模型可以调用:

text 复制代码
schedule_cron(cron, prompt, recurring, durable)

注册一条定时任务。

如果设置 durable=True,任务会写进:

text 复制代码
.scheduled_tasks.json

这样程序重启后,定时任务还能重新加载回来。

一个定时任务大致长这样:

json 复制代码
{
  "id": "cron_102314",
  "cron": "0 9 * * 1",
  "prompt": "检查项目状态并汇总未完成任务",
  "recurring": true,
  "durable": true
}

这里的 cron 是标准 5-field cron 表达式:

text 复制代码
minute hour day-of-month month day-of-week

比如:

text 复制代码
0 9 * * *       每天 09:00
*/10 * * * *    每 10 分钟
30 18 * * 1     每周一 18:30

3. 为什么调度器不直接调用模型

这份实现里最重要的设计点,其实不是 cron 表达式,而是触发后的交付方式。

它没有让 scheduler 线程直接调用模型,而是拆成了三层:

text 复制代码
scheduler 线程负责到点入队
queue processor 负责等待 Agent 空闲
agent loop 负责消费定时任务

为什么要这样设计?

因为模型调用和工具执行都会读写 messages

如果 scheduler 线程在任意时刻直接调用模型,就可能和用户当前输入、工具调用、后台通知发生并发冲突。

所以更稳的做法是:

text 复制代码
时间到了
    ↓
先放进队列
    ↓
等 Agent 空闲
    ↓
再安全触发一轮 agent_loop

这也是为什么实现里会出现:

  • 调度线程
  • 队列
  • queue processor
  • agent lock

它们一起保证"未来事件能回来",但"回来时仍然走统一入口"。

4. Cron 匹配和 durable 的意义

代码里还有两个值得补充的点。

第一,调度器会先校验 cron 表达式,再决定是否注册。

这一步不是可有可无的。

因为只要输入一条非法规则,系统后面就会一直带着一颗定时炸弹。最好的做法是注册时就把错误挡住,而不是等运行时才发现。

第二,durable=True 说明这一条规则不只活在内存里,还要保存到磁盘。

这跟前面的 Persistent Tasks 是同一个思路:

  • 内存适合当前运行态
  • 落盘适合跨会话恢复

如果一个定时规则注册完,程序一重启就消失,那它其实不算真正的长期调度能力。

5. 触发之后,系统里到底发生了什么

到点之后,定时任务会先被放进内部队列。

再往后,queue processor 会在 Agent 空闲时把它交给主循环。

最终,定时任务会被注入成一条普通消息:

text 复制代码
<scheduled-work>
[cron:cron_102314] 检查项目状态并汇总未完成任务
</scheduled-work>

模型并不需要学习一套全新的执行通道。

它只需要像处理普通用户输入一样,处理这条 scheduled work。

这一点非常重要,因为它意味着:

  • 调度器只负责"送达"
  • 主循环继续负责"理解、规划、调用工具、写结果"

职责边界非常清楚。

6. 一个具体例子

假设用户让 Agent:

text 复制代码
每隔 10 分钟检查一次后台测试是否结束,如果结束了就汇总结果

那么系统可以做两件事:

第一,用 Background Tasks 启动测试。

第二,用 Cron Scheduler 注册一个定时检查。

到点之后,主循环会再次被唤醒,并看到一条新的 scheduled work。此时它就能去:

  • 查询后台任务状态
  • 读取测试结果
  • 更新长期任务状态
  • 决定是否继续调试或通知用户

这说明调度层不是孤立存在的,它天然会和前两层协作。

7. 必要说明:cron 只是触发器,不是完整事件系统

这一层解决的是"未来如何触发"。

但 cron 只是最容易讲清楚的一种触发方式。

更完整的系统里,唤醒 Agent 的事件还可能来自:

  • webhook
  • 外部系统状态变化
  • 文件更新
  • 用户回访
  • 后台任务完成
  • 监控告警

所以更准确地说,Cron Scheduler 是"未来触发入口"的第一个版本。

先把时间触发跑通,后面再扩展到更多事件源,会更自然。

五、三层机制如何接在一起

把三层连起来看,长期运行的 Agent 其实有一条很清楚的数据流。

1. 先有任务状态,知道"要做什么"

用户或模型创建长期任务:

text 复制代码
模型调用 task_create
    ↓
TaskManager 生成 id
    ↓
写入 .tasks/task_<id>.json

这解决的是:

text 复制代码
任务不要只存在于上下文里

从这一刻起,系统里开始有了一份正式任务记录。

2. 再把长动作放到后台,知道"什么正在跑"

模型启动耗时命令:

text 复制代码
模型调用 background_run
    ↓
BackgroundManager 创建 task_id
    ↓
后台线程执行命令
    ↓
完成后写入通知队列
    ↓
下一轮注入 <background-results>

这解决的是:

text 复制代码
长动作不要阻塞主循环

也就是说,系统不只知道"要做什么",还知道"哪些事情正在独立推进"。

3. 最后用调度器补上未来触发,知道"什么时候再做"

模型注册定时任务:

text 复制代码
模型调用 schedule_cron
    ↓
CronScheduler 保存 job
    ↓
到点后 job 进入队列
    ↓
queue processor 等 Agent 空闲
    ↓
下一轮注入 <scheduled-work>

这解决的是:

text 复制代码
未来的工作要能重新进入主循环

于是系统就不再只依赖"用户此刻有没有发消息"。

4. 用一个完整场景把三层串起来

假设用户说:

text 复制代码
帮我修一个 CI 失败问题。先跑完整测试,今天先记着。如果今晚还没处理完,明天早上 9 点再提醒我继续。

这句话里,其实同时包含了三类需求。

第一,Agent 需要把"修复 CI 失败"记录成长期任务。

这时会用到 Persistent Tasks

第二,Agent 需要启动完整测试,但测试可能很慢,不能一直卡住主循环。

这时会用到 Background Tasks

第三,Agent 需要在未来某个时间点重新把这件事带回来。

这时会用到 Cron Scheduler

如果把它们合起来,整个链路就会变成:

text 复制代码
记录长期任务
    ↓
后台跑测试
    ↓
主循环继续做别的事
    ↓
测试结果回流
    ↓
如果任务仍未完成,明早定时唤醒
    ↓
继续推进同一个任务

到这里,Agent 的工作方式已经明显不是"问一句答一句"了。

它开始具备一种更像真实工作流的节奏:

  • 有状态
  • 有等待
  • 有未来触发

5. 为什么说这三层缺一不可

如果只做其中一层,系统都会有明显短板。

只有 Persistent Tasks

  • 能记住任务
  • 但不会异步跑长动作
  • 也不会在未来自动醒来

只有 Background Tasks

  • 能把长动作放后台
  • 但任务本身不一定能长期保存
  • 未来也没有统一触发机制

只有 Cron Scheduler

  • 能定时唤醒
  • 但被唤醒后可能没有稳定状态可读
  • 也没有后台执行通道可用

所以这三层不是彼此独立的小功能,而是一套连续工作机制:

text 复制代码
Persistent Tasks 保存"要做什么"
Background Tasks 执行"正在跑什么"
Cron Scheduler 触发"什么时候再做"

六、从教学实现走向工业级 Agent,还差哪些能力

这份实现是教学版本,已经把长期运行的主线跑通了。

如果继续往产品级 coding agent 靠,重点不只是"多加几个工具",而是把状态、执行、触发、安全边界和隔离环境一起设计。

Claude CodeCodex 提供了两个很好的参照。

它们表面上都有读代码、改文件、跑命令、调用工具、skills、hooks、MCP、子代理这些能力,但长期工作系统的组织方式并不一样。

Claude Code 更像是从"当前会话"出发,把一次工作会话不断增强:用 CLAUDE.md 和 auto memory 保存规则与经验,用 subagents 拆分探索和规划,用 background tasks 挂出长动作,再用 hooks、permissions 和 worktree isolation 约束执行过程。

Codex 更像是从"长期工作主线"出发,把 Agent 放进 thread、worktree、automations、inbox 这些结构里:AGENTS.mdMemories 提供稳定上下文,threads 承载持续推进的工作,worktrees 隔离并行执行,automations 负责未来触发和结果回流。

所以它们不是简单地"功能谁多谁少",而是在回答同一组架构问题:

text 复制代码
状态放在哪里?
长动作在哪里跑?
未来事件怎么回来?
并行执行怎么隔离?
结果最后交付到哪里?

后面的几层升级,都可以沿着这组问题展开。

1. 状态存储会从 JSON 文件升级成真正的状态系统

.tasks/.scheduled_tasks.json 很适合教学演示;到了真实系统里,任务、后台作业和定时规则通常会进入数据库或更完整的持久化层,并补上:

  • 权限和租户隔离
  • 审计日志
  • 事务或乐观锁
  • 查询索引
  • 失败恢复
  • 和外部系统的同步能力

产品级系统不会把所有长期状态都放在几个本地文件里,而是会把不同类型的状态拆开管理。

比如 Claude Code 里的 CLAUDE.md 和 auto memory,解决的是"下一次 session 还要带着哪些规则和经验开始工作";Codex 里的 AGENTS.mdMemoriesthreads,解决的是"这条长期工作主线未来还能依赖哪些稳定上下文"。

这几个概念可以分成三类来看。

CLAUDE.mdAGENTS.md 都更像"项目说明书":里面通常写项目约定、常用命令、代码风格、测试方式、注意事项。它们不是某一个任务的进度,而是 Agent 每次进入这个项目时都应该知道的长期规则。

auto memory 和 Memories 更像"经验记录":比如用户偏好、常用工作流、项目里踩过的坑、某些技术选择的原因。这类信息不一定适合写进项目说明书,但又值得在后续工作里复用。

threads 则更接近"工作主线":同一个长期问题,可以在一条 thread 里持续推进。它记录的不只是规则和经验,还包括这件事本身的上下文、过程和后续动作。

它们和我们这里的 .tasks/ 不是一一对应关系,但背后的原则是一样的:

text 复制代码
长期信息不能只塞进当前上下文。

2. 后台执行会从线程升级成队列、worker 和隔离环境

当前实现已经有最小的状态机和通知队列;更成熟的后台作业系统还需要处理更多运行期问题:

  • 取消
  • 重试
  • 超时策略
  • 优先级
  • 并发限制
  • 持久日志
  • worker 隔离
  • 更明确的失败分类

到这一步,BackgroundManager 就会从教学类演化成真正的 job queue。

理解产品级后台执行,不能只看系统是否支持"后台任务",更要看它如何回答四个问题:

text 复制代码
谁启动它?
它在哪里跑?
运行状态怎么被观察?
结果怎么回到 Agent 面前?

Claude Code 的 background tasks 更贴近当前 session。

可以把它理解成主会话旁边开了一条后台执行通道:

text 复制代码
当前 session 判断某个命令会很久
    ↓
把命令挂成 background task
    ↓
主会话继续读代码、规划或处理其他问题
    ↓
后台任务保留 task id 和输出
    ↓
后续由主会话追踪输出、读取结果、继续决策

这一类后台任务的核心,是让当前会话不中断。

后台任务虽然离开了同步工具调用链路,但它仍然服务于当前这次工作。它的结果通常还是被当前 session 拿回来继续用。

Codex 的 automations + background worktrees 是另一种形态:它不是只把命令挂到后台,而是把后台任务放进一个独立运行环境里:

text 复制代码
automation 按计划或规则触发
    ↓
系统为它准备 background worktree
    ↓
任务在隔离工作区里运行
    ↓
运行结果进入 thread、inbox 或 triage
    ↓
用户或 Agent 再决定是否继续推进、合并或处理

这一类后台任务进一步关注隔离和投递:后台工作不能污染当前工作区,结果也要有明确去处。

worktree 的作用就在这里。

如果后台任务会读写代码,它就不应该随便在用户正在编辑的目录里执行。否则它可能改到一半,刚好和用户当前修改冲突。独立 worktree 的意义,就是给后台任务一个自己的工作现场:它可以跑测试、改文件、生成结果,但不会直接搅乱主工作区。

所以产品级后台执行的重点,不只是"异步",而是:

text 复制代码
异步执行 + 状态可查 + 环境隔离 + 结果回流

对应到我们这份教学实现,BackgroundManager 已经做到了前两步:

  • 它能异步启动命令
  • 它能保存后台任务状态并把结果回流进上下文

但它还没有真正解决后两步:

  • 后台任务仍然运行在同一个工作目录里
  • 结果也只是注入下一轮 messages,没有更完整的 inbox、thread 或审计系统

这正是教学版和产品级系统之间的差距。

3. 调度系统会从 cron 升级成统一事件入口

当前实现主要演示 cron,但真实系统里的唤醒来源通常不只来自时间,还可能来自:

  • webhook
  • 外部系统事件
  • 文件变化
  • 队列消息
  • 监控告警
  • 用户重新进入线程

所以 cron 更像是"未来触发系统"的一个切面。更完整的形态通常会变成:

text 复制代码
触发条件
    时间 / 外部事件 / 用户回访 / 后台完成
        ↓
运行环境
    当前工作区 / 隔离环境 / 专用 worker
        ↓
执行策略
    权限 / 沙箱 / 审计 / hook
        ↓
结果交付
    当前线程 / 收件箱 / 工单 / PR / 通知系统

Codex 的 automations 就更接近这个方向:它不只是"到点提醒一下",而是把计划触发、后台运行、结果投递放进一条产品化链路里。

Claude Code 这边则更像是提供一组强能力模块:background tasks、hooks、worktree isolation、外部脚本和集成。要做更完整的长期自动化,通常需要把这些能力组合起来。

cron 只是第一步。真正的工业级调度系统,最后要回答的是:

text 复制代码
什么事件触发?
在哪个环境执行?
用什么权限执行?
结果交给谁?
失败以后怎么处理?

4. 真正的难点会变成"可靠性",而不只是"功能有没有"

教学版本最重要的是把主线跑通;产品级版本更关心的是长期运行时会不会稳定:

  • 进程重启后能不能恢复
  • 多个任务并发时会不会互相污染
  • 权限边界够不够清楚
  • 后台输出会不会把上下文塞爆
  • 失败后有没有重试和回滚机制
  • 用户能不能看懂系统当前到底在做什么

也就是说,工业化升级的重点,通常不是让 Agent"会更多功能",而是让它"长期工作时仍然可靠、可控、可恢复"。

小结

这一篇讲的三个机制,本质上是在补 Agent 长期运行的三条链路:

  • Persistent Tasks 让状态外置:任务不再只依赖当前对话,而是有了可恢复的记录。
  • Background Tasks 让执行解耦:长动作可以先跑起来,不必一直占着主循环。
  • Cron Scheduler 让触发回流:未来的工作可以在合适的时间重新回到 Agent 面前。

这三层合在一起,Agent 才开始从"当前对话里的助手",变成一个能持续推进工作的系统。

它能记住要做什么,能等待正在发生的事,也能在未来重新接上未完成的工作。

相关推荐
用户701720739001 小时前
密码学之分组密码
算法
MrZhao4001 小时前
Agent 长上下文处理机制:Context Compact 与 Memory 的协同
算法
h_a_o777oah1 小时前
【动态规划】区间 DP :三层循环逻辑与模板实现细节(洛谷 P1880)
c++·算法·动态规划·acm·区间dp·化环为链·石子合并
从此以后自律1 小时前
迪杰斯特拉
算法
QiLinkOS2 小时前
第三视觉理解徐玉生与他的商业活动(32)
大数据·c++·人工智能·算法·开源协议
KaMeidebaby2 小时前
卡梅德生物技术快报|实操手册:CXCL4 蛋白原核表达全套工艺,两步层析去除蛋白多聚体附完整电泳数据
人工智能·算法·机器学习·架构·spark
AIGCmagic社区2 小时前
Unlimited OCR 论文精读:R-SWA 如何实现一次性长文档解析
人工智能·算法·aigc
鹏易灵2 小时前
C++——7.类与对象,掌握封装、继承、多态.详解
开发语言·c++·算法
Mortalbreeze2 小时前
深入 Linux 线程机制(三):线程互斥——竞争条件与互斥锁的本质
linux·运维·服务器·c++·算法