Background Tasks - 后台任务与异步执行

3262 字
16 分钟
Background Tasks - 后台任务与异步执行

L13 要回答一个问题:如何在保持 Agent 主循环单线程的前提下,让慢命令不阻塞工作流?

L11 给了我们骨架:三种恢复路径 + 有限重试 + 优雅失败。但真实 Agent 执行慢命令(npm install、pytest、docker build)时,模型在等待期间什么都做不了——用户还想继续别的工作,却被整轮流程堵住。怎么做?

本章讲三个机制:

机制解决什么问题核心设计
后台执行线慢命令不阻塞主循环background_run 立刻返回 task_id
通知队列后台完成后告知主循环drain_notifications + 摘要注入
任务表分离执行状态 vs 结果投递RuntimeTaskRecord + Notification

这三个机制叠加在 L11 的 Error Recovery 上,循环本身不变。理解 L13,就理解了 Agent 如何”并行等待不阻塞主循环”。

一、核心命题:主循环只有一条#

“主循环仍然只有一条,并行的是等待,不是主循环本身。”

这句话概括了 Background Tasks 的本质。不是让主循环变多线程——而是让”慢执行”移到后台,主循环继续推进别的事情。

问题背景:慢命令阻塞#

同步执行慢命令会导致:

问题说明
模型等待期间无事可做调用 API 后等待 pytest 结果,整轮流程卡住
用户被整轮流程堵住想继续别的工作,却被一个慢命令阻塞

错误理解:后台就是另一条主循环。

正确理解:主循环保持单线程,后台是另一条执行线。

主循环流程变更#

flowchart TB subgraph MainLoop["主循环 (单线程)"] A[模型发起 background_run] --> B[登记 RuntimeTaskRecord] B --> C[立刻返回 task_id] C --> D[继续别的工作] E[下一轮调用前] --> F[drain_notifications] F --> G[摘要 append 到 messages] G --> H[调用模型] H --> I[模型看到后台结果] end subgraph Background["后台执行线 (独立线程)"] J[真正执行命令] --> K{执行结果} K -->|成功| L[status: completed] K -->|失败| M[status: failed] K -->|超时| N[status: timeout] L & M & N --> O[写入 Notification] O --> P[完整输出写磁盘文件] end C --> J P --> F style MainLoop fill:#e1f5fe style Background fill:#fff3e0

流程说明

  • 主循环:background_run → 登记任务 → 立刻返回 task_id → 继别工作
  • 后台执行线:真正执行命令 → 写入通知 → 写磁盘文件
  • 下一轮:drain_notifications → 摘要注入 → 模型收到结果

验证你的理解#

Q1:为什么”后台是另一条主循环”是错误理解?

:::details 答案 主循环必须保持单线程,否则状态管理混乱。

如果多条主循环:

  1. 多个 messages[] → 状态不一致
  2. 多个 tool_use → 权限检查混乱
  3. 多个 stop_reason → 循环控制失控

正确设计:主循环单线程,后台是另一条执行线。并行的是等待时间,不是主循环本身。 :::

二、数据结构:任务表 vs 通知队列#

后台任务至少需要”任务表 + 通知队列”两块状态。职责分离。

RuntimeTaskRecord:执行状态管理#

task = {
"id": "a1b2c3d4",
"command": "pytest",
"status": "running", # running/completed/failed/timeout
"started_at": 1710000000.0,
"result_preview": "",
"output_file": "",
}

职责:管执行状态(谁在跑、跑到哪)。

字段用途
idtask_id,唯一标识
command执行的命令
status当前状态(running/completed/failed/timeout)
started_at启动时间
result_preview摘要(不超过500字)
output_file完整输出文件路径

Notification:结果投递通道#

notification = {
"type": "background_completed",
"task_id": "a1b2c3d4",
"status": "completed",
"preview": "tests passed", # 只放摘要,不放全文
}

职责:管结果投递(完成了、通知主循环)。

字段用途
type通知类型(background_completed)
task_id对应任务ID
status最终状态
preview摘要(不超过500字)

职责分离原则#

状态块职责存储位置
tasks(任务表)执行状态管理内存 + 磁盘
notifications(通知队列)结果投递通道内存队列

为什么分离?

不分离的问题分离的好处
任务状态和通知混在一起 → 状态混乱执行状态独立管理 → 清晰
drain 需要遍历所有任务 → 效率低drain 只遍历通知 → 高效
任务持久化包含通知 → 冗余任务持久化不含通知 → 简洁

验证你的理解#

Q2:为什么 notifications 只放 preview,不放完整输出?

:::details 答案 防止上下文撑爆 + 防止注意力稀释。

如果通知放完整输出:

  1. pytest 可能输出数千行 → messages[] 撑爆
  2. 模型注意力被稀释 → 核心任务权重下降
  3. 多个后台任务完成 → 上下文爆炸

正确设计:通知只放 preview(500字摘要),完整输出写磁盘文件。模型需要全文时用 read_file 读取。 :::

三、通知机制:drain + 摘要注入#

后台任务完成后,通知队列有消息——下一轮主循环需要”排空”通知。

drain_notifications 流程#

flowchart TB subgraph Drain["drain_notifications"] D1[检查 notifications 队列] --> D2{有通知?} D2 -->|是| D3[取出 notification] D3 --> D4[生成摘要消息] D4 --> D5[append 到 messages] D5 --> D2 D2 -->|否| D6[返回] end subgraph NextRound["下一轮"] N1[调用模型] --> N2[模型看到后台结果] end D6 --> N1 style D5 fill:#e8f5e9

流程

  1. drain_notifications 排空通知队列
  2. 把通知摘要 append 到 messages (user role)
  3. 再调用模型
  4. 模型收到后台结果

摘要注入位置#

# 主循环入口
def agent_loop():
# 1. 先 drain_notifications 排空通知队列
notifications = drain_notifications()
for notif in notifications:
# 2. 把通知摘要 append 到 messages (user role)
messages.append({
"role": "user",
"content": f"Background task {notif['task_id']} completed: {notif['preview']}"
})
# 3. 再调用模型
response = call_llm(messages)

关键点:通知注入在调用模型前——模型本轮能看到后台结果。

验证你的理解#

Q3:为什么通知注入在调用模型前,而不是调用后?

:::details 答案 模型本轮能看到后台结果,不需要等到下一轮。

如果注入在调用模型后:

  1. 本轮模型看不到后台结果
  2. 用户问”后台任务跑完了吗” → 模型不知道
  3. 需要下一轮才能看到 → 响应延迟

正确设计:drain 在调用模型前 → 模型本轮能看到结果 → 实时响应。 :::

四、最小工具:background_run + background_check#

后台任务需要两个工具:发起后台任务 + 查询任务状态。

background_run:发起后台任务#

def background_run(command: str) -> str:
# 1. 登记 RuntimeTaskRecord
task_id = generate_task_id()
task = {
"id": task_id,
"command": command,
"status": "running",
"started_at": time.time(),
}
tasks[task_id] = task
# 2. 写磁盘持久化
save_task_to_disk(task)
# 3. 启动后台线程执行
start_background_thread(command, task_id)
# 4. 立刻返回 task_id
return task_id

关键设计:立刻返回 task_id,不是同步卡住。

background_check:主动查询状态#

def background_check(task_id: str) -> dict:
task = tasks.get(task_id)
if not task:
return {"error": "Task not found"}
return {
"status": task["status"],
"preview": task["result_preview"],
"output_file": task["output_file"],
}

用途:模型主动查询任务状态(notification 已 drain 或未 drain)。

场景用什么
notification 已 drain模型已看到结果 → 不需要 background_check
notification 未 drain模型调用 background_check 查询

验证你的理解#

Q4:background_run 为什么立刻返回 task_id,而不是等待执行完成?

:::details 答案 主循环不阻塞,继续推进别的工作。

如果等待执行完成:

  1. pytest 可能跑10分钟 → 主循环阻塞10分钟
  2. 模型在等待期间什么都做不了
  3. 用户被整轮流程堵住

正确设计:background_run 立刻返回 task_id → 主循环继续 → 后台独立执行 → 下一轮看到结果。 :::

五、回顾:L13 在循环上叠加了什么#

回到开头的问题:如何在保持 Agent 主循环单线程的前提下,让慢命令不阻塞工作流?

答案:

叠加机制代码位置循环的变化
后台执行线background_run 工具不变——只是返回 task_id
通知队列drain_notifications不变——只是 messages 增加
任务表分离RuntimeTaskRecord不变——只是状态管理

循环本身仍是 L01 的 while True + stop_reason + messages[]。L13 只是在”执行层”叠加了后台机制。

与后续课程的关系#

课程关系
L11 Error Recovery后台任务失败 → 通知队列写入失败状态 → drain 注入失败消息
L12 Task Systemtask(工作板)vs background task(运行作业)——职责分离
L14 Cron Scheduler定时任务完成后同样写入通知队列

核心洞察:Background Tasks 是”等待不阻塞”的机制。理解 L13,就理解了 Agent 如何”主循环单线程 + 后台并行等待”。后续课程都在扩展后台机制——定时任务、任务系统。

六、心智模型升级:从 L0 到 L2#

读完这篇文章,你的理解经历了怎样的升级?

L13 核心概念的理解升级路径#

概念L0(表面)L1(关联)L2(深层)
核心命题”后台任务""主循环单线程 + 后台执行线""主循环只有一条,并行的是等待,不是主循环本身。类比 TCP 连接:发送方继续发数据,不等 ACK 回来——滑动窗口机制”
任务表分离”两个数据结构""RuntimeTaskRecord + Notification""职责分离——任务表管执行状态(谁在跑),通知队列管结果投递(完成了)。类比 Git:index(暂存区)vs HEAD(提交历史)——职责分离”
通知机制”后台完成后告诉主循环""drain_notifications + 摘要注入""通知只放 preview——防止上下文撑爆 + 防止注意力稀释。类比消息队列:只放事件摘要,不放完整数据。模型需要全文时 read_file”
后台工具”background_run 和 background_check""发起任务 + 查询状态""background_run 立刻返回 task_id——不是同步卡住。类比异步 API:Promise 立刻返回,await 等结果。主循环继续推进,不阻塞”

如何检验你达到了 L2?#

用这个问题自测:

如果通知队列放完整输出(数千行 pytest 日志),会发生什么?完整链条是什么?

:::details L2 层级回答 上下文撑爆 + 注意力稀释 + 多任务累积爆炸。

完整链条

  1. pytest 输出数千行日志
  2. notification 包含完整输出
  3. drain 注入 messages → messages[] 撑爆(数万 tokens)
  4. Transformer 注意力权重归一化 → pytest 日志权重累加
  5. 核心任务权重被稀释 → 输出可能跑偏
  6. 多个后台任务完成 → 通知队列累积 → messages[] 更大
  7. 触发 Context Compact → 摘要生成 → 丢失推理细节

正确设计:通知只放 preview(500字摘要),完整输出写磁盘文件。模型需要全文时 read_file 读取。 :::

本章目标:让每个核心概念都从 L0 升级到 L2。理解 L13,不只是记住任务表和通知队列,而是掌握”如何在单线程主循环上并行等待”的设计原则。

七、复习可视化#

主循环与后台执行线的关系#

flowchart TB subgraph MainLoop["主循环 (单线程)"] A[模型发起 background_run] --> B[登记 RuntimeTaskRecord] B --> C[立刻返回 task_id] C --> D[继续别的工作] E[下一轮调用前] --> F[drain_notifications] F --> G[摘要 append 到 messages] G --> H[调用模型] H --> I[模型看到后台结果] end subgraph Background["后台执行线 (独立线程)"] J[真正执行命令] --> K{执行结果} K -->|成功| L[status: completed] K -->|失败| M[status: failed] K -->|超时| N[status: timeout] L & M & N --> O[写入 Notification] O --> P[完整输出写磁盘文件] end C --> J P --> F style MainLoop fill:#e1f5fe style Background fill:#fff3e0

时间线与并行关系#

flowchart LR subgraph Timing["时间线"] T1[T1: 发起后台任务] --> T2[T2: 主循环继续工作] T2 --> T3[T3: 后台执行中] T3 --> T4[T4: 下一轮 drain] T4 --> T5[T5: 模型收到结果] end subgraph Parallel["并行的是什么"] P1[主循环的等待时间] -.-> P2[后台的执行时间] end T3 --> P1 T3 --> P2 style Timing fill:#f3e5f5 style Parallel fill:#e8f5e9

数据流向#

flowchart TB subgraph Storage["存储层"] RT[".runtime-tasks/<id>.json<br/>RuntimeTaskRecord"] LOG[".runtime-tasks/<id>.log<br/>完整输出"] end subgraph Memory["内存状态"] TASKS["tasks 字典<br/>执行状态管理"] NOTIF["notifications 队列<br/>结果投递通道"] end subgraph Agent["模型层"] MSG["messages[]<br/>摘要注入 (user role)"] end BackgroundRun --> TASKS BackgroundRun --> RT BackgroundExecute --> LOG BackgroundExecute --> NOTIF Drain --> NOTIF Drain --> MSG TASKS --> RT LOG --> MSG style Storage fill:#fce4ec style Memory fill:#e3f2fd style Agent fill:#f1f8e9

前台 vs 后台对比#

flowchart TB subgraph Foreground["前台执行"] F1[主循环发起工具调用] --> F2[同步等待结果] F2 --> F3[结果返回] F3 --> F4[主循环继续] end subgraph Background["后台执行"] B1[主循环发起 background_run] --> B2[立刻返回 task_id] B2 --> B3[主循环继续别的工作] B3 --> B4[后台独立执行] B4 --> B5[通知队列写入] B5 --> B6[下一轮 drain] B6 --> B7[模型收到结果] end F1 --> F2 --> F3 --> F4 B1 --> B2 --> B3 --> B4 --> B5 --> B6 --> B7 style F2 fill:#ffebee style B2 fill:#e8f5e9

Task vs Background Task 边界#

flowchart LR subgraph Task["L12 Task System"] T1[工作目标] --> T2[做什么] T2 --> T3[谁依赖谁] T3 --> T4[进度如何] T4 --> T5[类比: 工作板] end subgraph Background["L13 Background Task"] B1[运行作业] --> B2[哪个命令在跑] B2 --> B3[什么状态] B3 --> B4[结果何时回来] B4 --> B5[类比: 运行中的作业] end style T5 fill:#e1f5fe style B5 fill:#fff3e0

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
Background Tasks - 后台任务与异步执行
https://firefly.cuteleaf.cn/posts/learn-claude-code/13-background-tasks/
作者
AltumSisy
发布于
2026-04-22
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
AltumSisy
Hello, I'm AltumSisy.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
32
分类
3
标签
25
总字数
68,347
运行时长
0
最后活动
0 天前

文章目录