长命令运行时,Agent 怎样继续处理其他工作

本文是「从零理解 Claude Code:20 个 Agent Harness 机制」系列的第 13 篇。

对应源码:s13_background_tasks

源码仓库:shareAI-lab/learn-claude-code

设想 Agent 正在初始化一个前端项目。

它需要执行依赖安装,还要检查 package.json、阅读构建脚本、确认环境变量。依赖安装可能需要几分钟,读取配置文件只需要几毫秒,若所有工具调用都同步执行,Agent 发起安装命令后,就只能等终端返回结果,后面的检查工作也得暂停。

s13 将慢操作放进后台执行。Agent 拿到任务已启动的反馈后,可以继续调用其他工具,后台命令完成时,结果会作为一条通知重新放回对话。

一、同步工具调用为什么会堵住主循环

上一章中的工具调用是同步的。模型请求执行命令,程序执行命令,拿到结果后再交给模型决定下一步。

这个流程适合读取文件、搜索文本、查看 Git 状态等短操作。遇到依赖安装、全量测试、镜像构建或部署命令时,主循环会一直停在工具执行阶段。

npm install 为例,Agent 已经知道接下来还要阅读项目配置,却必须等安装结束后才能继续。等待期间没有新工具结果,也不会发起下一轮模型调用。

本章的代码改变了工具的执行策略,短操作仍然同步执行,慢操作被交给后台线程,主循环继续处理当前任务。

操作类型 示例 处理方式
短操作 读取文件、搜索代码、查看状态 同步执行并立即返回结果
慢操作 安装依赖、运行测试、构建项目、部署 后台执行,稍后注入通知

二、程序怎样判断命令是否需要转入后台

bash 工具增加了一个 run_in_background 参数,模型可以在调用工具时明确要求后台执行。

如果模型没有指定该参数,教学代码会根据命令中的关键词做兜底判断:

python 复制代码
def should_run_background(tool_name: str, tool_input: dict) -> bool:
    if tool_input.get("run_in_background"):
        return True

    return is_slow_operation(tool_name, tool_input)

关键词列表包括 installbuildtestdeploycompile 等。它们覆盖了依赖安装、构建、测试和部署这类通常耗时较长的命令。

这种启发式判断适合教学演示,实际使用时要更谨慎。

例如,echo test 很快完成,却因为包含 test 被判定为慢操作。某个运行很久的自定义脚本如果没有包含这些关键词,也可能继续走同步执行。代码里的 run_in_background=False 也不会强制命令留在前台,因为它仍会继续进入关键词判断。

生产系统更适合让模型或调用方明确指定执行策略,再用运行时间、命令类型和资源占用作为辅助判断。

三、后台任务启动后,为什么要先返回占位结果

后台任务启动时,程序会生成一个任务 ID,并在内存中记录命令、原始工具调用 ID 和任务状态。

后台工作线程负责执行真正的工具调用,主循环则立刻向模型返回一条占位工具结果:

css 复制代码
if should_run_background(block.name, block.input):
    bg_id = start_background_task(block)

    results.append({
        "type": "tool_result",
        "tool_use_id": block.id,
        "content": f"[Background task {bg_id} started] "
                   f"Result will be available when complete.",
    })

这条占位结果有两个作用。

第一,模型知道命令已经启动,可以继续安排其他工作。

第二,原始工具调用需要尽快完成消息配对。模型发起 npm install 时,会附带一个工具调用 ID。程序启动后台线程后,立刻用这个 ID 返回一条占位结果,告诉模型命令已经开始执行。此时,这次工具调用已经完成了自己的协议流程。

后台任务结束后,完整输出不能再使用原来的工具调用 ID 返回。原 ID 已经对应过占位结果,再追加一份工具结果会让同一次调用出现两份结果。教学代码因此为后台任务单独分配 bg_0001 这类任务编号,并通过任务完成通知把结果带回对话。

把后台任务的状态和消息关系放在一起看,会更容易理解为什么完成通知需要单独注入。

四、后台结果怎样重新进入模型上下文

后台线程执行结束后,会把结果写入内存中的结果字典,并把任务状态标记为已完成。

主循环在处理工具调用后,会检查是否有已经完成的后台任务。发现结果后,程序构造一条独立通知:

xml 复制代码
<task_notification>
  <task_id>bg_0001</task_id>
  <status>completed</status>
  <command>npm install</command>
  <summary>...</summary>
</task_notification>

这条通知和当前工具结果一起组成一条新的用户消息,随后交给模型。

设想第一轮中,Agent 把依赖安装放进后台,同时读取了项目配置。第二轮里,安装完成通知和配置文件内容会一起进入上下文。模型可以根据两部分信息继续判断,例如确认依赖安装成功后再运行构建命令。

通知不复用原始工具调用 ID,避免与此前的占位结果冲突。后台任务完成是一次独立事件,使用任务 ID 更容易追踪。

五、教学实现还缺少哪些后台任务能力

设想 Agent 把 npm test 放到后台执行,然后趁测试运行的时间读取 package.json、检查测试脚本。

测试启动后,Agent 已经收到一条提示:后台任务 bg_0001 正在运行。它继续分析配置,随后认为当前任务已经可以结束,于是返回最终回答。

这时测试可能还在跑。

如果用户直接退出程序,后台线程会跟着主进程结束,测试也会中断,任务状态和命令输出都只保存在内存里,下一次启动 Agent 时,程序不知道刚才是否跑过测试,也没有地方可以查看测试运行到哪一步。

即使用户没有退出程序,后台结果也不一定能立刻回到模型上下文。教学代码只会在处理一轮工具调用后检查后台任务是否完成。模型如果在下一轮直接返回普通文本,没有再调用工具,主循环就结束了,bg_0001 的完成通知会继续留在内存中,等待之后某次工具调用才有机会被收集。

任务完成后,程序还会把完整输出缩短为前 200 个字符的摘要:

text 复制代码
<task_notification>
  <task_id>bg_0001</task_id>
  <status>completed</status>
  <summary>前 200 个字符的命令输出</summary>
</task_notification>

假设 npm test 输出了一千多行,摘要里只显示最后的报错标题。Agent 想继续确认哪个测试失败、错误栈指向哪里时,当前代码已经没有读取完整后台输出的入口。通知注入后,任务记录和完整输出都会从内存字典中删除。

还有一个容易误解的地方:任务状态写成 completed,只表示后台线程已经执行结束。

如果 npm test 返回了错误,run_bash() 会把标准输出和错误输出拼成一段文本返回。后台线程依然会把任务标记为完成,因为它没有记录命令退出码。也就是说,completed 在这份教学代码里表示命令已经结束,不表示命令执行成功。

场景 教学代码中的结果 实际系统需要补充的能力
用户关闭 Agent 守护线程和内存状态一起消失 独立进程、任务持久化、恢复机制
后台命令完成 只能在后续工具调用后尝试注入通知 独立通知队列和主动唤醒
命令输出很长 只把前 200 个字符交给模型 保存完整日志,并提供读取接口
命令执行失败 线程结束后仍标记为 completed 保存退出码、成功或失败状态、错误类型
命令是否后台执行 显式参数加关键词判断 更明确的执行策略和资源控制

README 中提到,真实 Claude Code 对后台 Shell 任务提供了更完整的生命周期管理:输出可以重定向保存,后续能够读取日志,也可以停止任务,完成事件通过通知队列送回对话。

s13 保留的是最小流程:慢命令不再堵住主循环,完成后尝试将结果通知模型。要让后台任务真正适合长期运行,还需要解决任务退出、状态保存、结果读取和失败判断这些问题。

六、小结

同步工具调用适合短操作。命令需要运行几分钟时,主循环若一直等待,Agent 就无法同时读取文件、检查配置或推进其他任务。

s13 把慢命令交给后台线程,并先返回一条任务已启动的占位结果。命令完成后,结果以独立通知重新进入对话。这样,模型知道任务正在执行,也能在等待期间继续处理其他工作。

当前教学实现还没有持久化后台任务、保存完整可读取日志、区分命令成功与线程结束,也没有主动把通知送回已经结束的循环。后续设计后台任务时,除了考虑怎样启动命令,也要确认进程退出后任务怎么办、命令失败怎样表示、完整输出由谁保存。

下一章会处理定时执行的问题。后台任务解决的是一项工作已经开始后怎样不阻塞,定时调度关注的是任务应该在什么时候开始。

相关推荐
橘子星1 小时前
RAG 实战:3 步将整本《天龙八部》存入向量数据库(一)
javascript·人工智能
廋到被风吹走1 小时前
【AI】本周AI领域三大重磅事件
人工智能
萌动的小火苗1 小时前
深度学习中的损失函数与优化算法基础知识
人工智能·深度学习·算法
A15362551 小时前
五金批发电商业财一体化 ERP 推荐:打通订单、库存、财务对账
大数据·运维·人工智能·零售
把所有砖敲烂2 小时前
GLM 5.2 核心能力与效果实测全景
人工智能
神奇霸王龙2 小时前
Qwen3.7-Max屠榜:推理成本仅GPT-5.5的1/25
人工智能·python·gpt·ai·aigc·ai编程
StarkCoder2 小时前
AI 会做多、看少、不收尾:七种失效和拦住它们的办法
人工智能·架构
happyprince2 小时前
03_OpenCodeReview 深刻不忘观:硬约束 × 动态决策的设计哲学
人工智能
持力行2 小时前
AI编程CRUD类项目实操
大数据·人工智能·ai编程