s13:后台任务(Background Tasks)
s13:后台任务(Background Tasks)
核心思想
基于线程的后台任务执行系统——核心目标是让耗时操作异步执行,避免阻塞 Agent 的主循环,并通过通知机制将结果异步注入对话流。
关键区别:s13 的后台执行 ≠ 自动触发(那是 s14 的事)。s13 解决的是「命令太慢会卡住主循环」的问题。
1. 后台任务生命周期
工具调用请求
│
▼
should_run_background() ──否──→ 同步执行(原有路径)
│
是
▼
start_background_task()
│
├─→ 分配 bg_id(bg_0001, bg_0002...)
├─→ 创建守护线程(daemon=True)
└─→ 立即返回占位符(不等待)
│
▼
主循环继续(处理其他工具调用或下一轮 LLM)
│
▼
下一轮循环开始时
│
▼
collect_background_results()
│
├─→ 检查已完成的后台任务
├─→ 生成 通知
└─→ 注入到 user 消息中
2. 后台任务存储结构
# 全局递增计数器,生成唯一的后台任务 ID
_bg_counter = 0
# bg_id → {tool_use_id, command, status}
background_tasks: dict[str, dict] = {}
# bg_id → output(执行结果字符串)
background_results: dict[str, str] = {}
# 线程锁,保护共享字典的读写操作
background_lock = threading.Lock()
四个核心数据结构的作用:
| 变量 | 类型 | 作用 |
|---|---|---|
_bg_counter |
int |
全局递增计数器,保证每个 bg_id 唯一 |
background_tasks |
dict[str, dict] |
任务元信息:记录每个后台任务的 ID、对应工具调用 ID、命令内容、运行状态 |
background_results |
dict[str, str] |
任务结果:bg_id → 执行输出的字符串 |
background_lock |
threading.Lock |
线程锁:保护上面两个字典的并发读写安全 |
3. 慢操作检测 — is_slow_operation()
判断逻辑
教学内容做了简化,只对 bash 工具进行检测(其他工具通常快速返回)。具体步骤:
- 提取命令字符串并转为小写
- 检查是否包含已知的慢操作关键词
慢操作关键词分类
| 类别 | 关键词 | 说明 |
|---|---|---|
| 包管理器 | pip install, npm install, cargo build |
安装依赖通常需要下载+编译 |
| 构建工具 | build, compile, make |
编译项目耗时较长 |
| 测试框架 | test, pytest |
测试套件可能跑很久 |
| 部署操作 | deploy, docker build |
镜像构建和部署 |
| 通用长时 | install |
兜底匹配 |
代码实现
def is_slow_operation(tool_name: str, tool_input: dict) -> bool:
"""判断一个工具调用是否属于慢操作"""
if tool_name != "bash":
return False # 只检测 bash 工具
cmd = tool_input.get("command", "").lower()
slow_keywords = [
"install", "build", "test", "deploy", "compile",
"docker build", "pip install", "npm install",
"cargo build", "pytest", "make"
]
return any(kw in cmd for kw in slow_keywords)
4. 触发决策 — should_run_background()
一条命令是否进入后台执行,采用两级判断策略:
- 显式优先:模型在工具调用参数中传了
run_in_background=True→ 直接后台 - 启发式兜底:模型未指定时,通过关键词自动判断(命令含
install/build/test等)→ 后台
def should_run_background(tool_name: str, tool_input: dict) -> bool:
# 第一级:模型显式指定 run_in_background: true
if tool_input.get("run_in_background"):
return True
# 第二级:模型未指定时,通过关键词检测自动判断
return is_slow_operation(tool_name, tool_input)
设计原则:显式请求永远优先于启发式规则。模型"知道"该后台时就让它自己决定;模型"不知道"时系统兜底自动判断。
5. 后台任务启动 — start_background_task()
核心公式
后台执行 = 守护线程(Daemon Thread)+ 生命周期字典
把工具执行包进一个 worker 函数,丢进 daemon=True 的线程里,返回一个 bg_id 给调用方。
什么是守护线程?
守护线程(Daemon Thread)是一种特殊线程,其生命周期与主线程绑定——主线程结束时,所有守护线程自动跟着退出,不会阻止进程关闭。这非常适合辅助任务:可以随时中断、不需要优雅退出。
代码实现
def start_background_task(block) -> str:
"""启动一个后台任务,返回 bg_id"""
global _bg_counter
_bg_counter += 1
bg_id = f"bg_{_bg_counter:04d}" # 格式:bg_0001, bg_0002, ...
cmd = block.input.get("command", block.name)
# 闭包:捕获 block 和 bg_id,在线程内执行实际工具调用
def worker():
result = execute_tool(block) # 实际执行工具
with background_lock: # 加锁写结果
background_tasks[bg_id]["status"] = "completed"
background_results[bg_id] = result
# 先注册任务状态(running),再启动线程
with background_lock:
background_tasks[bg_id] = {
"tool_use_id": block.id,
"command": cmd,
"status": "running",
}
thread = threading.Thread(target=worker, daemon=True)
thread.start()
print(f" \033[33m[background] dispatched {bg_id}: {cmd[:40]}\033[0m")
return bg_id
执行顺序要点:
- 先分配
bg_id - 先把状态写入字典(
status: "running") - 再启动线程
- 立即返回
bg_id(不等待线程完成)
6. 结果收集 — collect_background_results()
收集机制
每轮循环结束时扫描已完成的任务,把结果格式化成 <task_notification> 注入对话。这样 LLM 在后续轮次中能被动感知后台任务的完成。
为什么用 <task_notification> 而不是伪装成 tool_result?
因为 Messages API 要求一个 tool_use 严格对应一个 tool_result——原来的调用已经用占位符回复过了,后台结果只能以"新事件"的身份出现,不能冒充之前的 tool_result。
主循环的行为变化:快同步、慢异步
Turn 1: bash "npm install" (后台)
↓
占位 tool_result → LLM 继续(不等待安装完成)
↓
Turn 2: read_file "package.json" (同步)
↓
文件结果 + collect 发现 bg_0001 已完成
↓
注入 到 user 消息
↓
LLM 在同一条消息里看到:
「文件内容 + 安装完成通知」→ 继续下一步
效果:慢操作在后台跑着,快操作照常同步执行——两不耽误。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 茯茶养生人的博客!










