246. 技术:HarnessX 第 13 课:让慢 bash 先进后台,完成后用通知回到 Agent Loop

2026.06.30

·技术harnessx

20260630_3.webp|1304

一些注意点:

  • S13 没有新增独立工具,只是在 bash 上增加 run_in_background
  • 是否后台执行,优先交给模型判断。
  • 后台启动后,HarnessX 先回填一条 role: "tool" 占位结果,把 bg_0001 交给模型。
  • 后台完成后,HarnessX 在下一轮请求模型前,把完成结果注入成新的 role: "user" 消息。
  • 注入内容用 <task_notification> 包住,里面放同一个 task_id、状态、命令和输出摘要。
  • 模型能感知 bg_0001,但只能通过 messages 感知,不能直接读取后台任务表。
  • bg_0001 只保证当前 Node.js 进程内唯一,不做跨进程恢复。
  • 输出太长时写入 .task_outputs/background/,通知里只放预览和文件路径。

246. 技术:HarnessX 第 13 课:让慢 bash 先进后台,完成后用通知回到 Agent Loop 图表 1

重点

第 13 课只改变 bash 的执行方式:

  • 快命令继续前台执行。
  • 慢命令可以设置 run_in_background=true,放到后台执行。
  • Agent 立即拿到任务 ID,继续处理其他工作。
  • 后台完成后,通过 <task_notification> 把结果送回 Agent Loop。

246. 技术:HarnessX 第 13 课:让慢 bash 先进后台,完成后用通知回到 Agent Loop 图表 2

用洗衣机理解等待,用号码牌理解通知

后台任务很像洗衣机。

把衣服放进去,按下启动后,你可以先去做饭、回消息、看论文。洗完以后,洗衣机再“滴滴滴”通知你。你不会站在洗衣机前干等 30 分钟。

Agent 的 bash 工具也一样:

  • read_filegit status 通常很快,适合在前台等待结果。
  • pip install torchnpm run build 可能需要几分钟,适合放到后台。
  • 慢命令运行期间,Agent 可以继续读取文件、分析代码或执行其他工具。
  • 命令完成后,再把结果通知 Agent。

这不是让 LLM 在后台继续生成 token,而是避免 Agent Loop 卡在一次慢工具调用上。

再从运行机制看,慢命令也像去窗口办理一项耗时业务。

同步执行时,你把材料交给窗口后一直站着等。

后台执行时,你先拿到号码牌。窗口继续处理,你可以先去办别的事;处理完成后,系统再叫号。

246. 技术:HarnessX 第 13 课:让慢 bash 先进后台,完成后用通知回到 Agent Loop 图表 3

在 Node.js 里,“后台”不等于开一个新的 JavaScript 线程。

当前实现用 exec 启动子进程,但 Agent Loop 不 await 它。子进程继续跑,主循环马上拿到 bg_0001 往下走。

这层放在 Agent 架构的哪里

后台任务位于“权限门之后、真正执行 bash 的位置”。

246. 技术:HarnessX 第 13 课:让慢 bash 先进后台,完成后用通知回到 Agent Loop 图表 4

各层职责很清楚:

  • DeepSeek:
    • 通过工具参数表达“这条命令可以后台跑”。
  • 权限门:
    • 仍然先判断命令能不能执行。
  • 后台任务表:
    • 记录进程内的任务生命周期和结果。
  • Agent Loop:
    • 每轮请求模型前收集已完成任务。
  • messages:
    • 同时承载启动占位结果和后续完成通知。

后台任务表里保存的是一张运行时记录,不是一个可被模型直接调用的新工具。

246. 技术:HarnessX 第 13 课:让慢 bash 先进后台,完成后用通知回到 Agent Loop 图表 5

哪些 bash 会进入后台

主路径是模型显式设置 run_in_background=true

模型没传这个字段时,教学版再用慢命令关键词兜底。

246. 技术:HarnessX 第 13 课:让慢 bash 先进后台,完成后用通知回到 Agent Loop 图表 6

false 必须强制前台。显式选择的优先级高于关键词猜测。

子代理暂时不开放后台执行。否则父 Agent 的通知队列和子代理的生命周期会混在一起,这不是当前课程要解决的问题。

bg_0001 怎么保证多个任务不会串

bg_0001 是当前 Node.js 进程里的后台任务主键

每次启动后台命令时,计数器先加一,再生成 bg_0001bg_0002bg_0003。这个编号生成动作是同步完成的,所以同一个进程里不会有两个任务拿到相同 ID。

编号生成后,HarnessX 先把任务记录写进 backgroundTasks Map,再启动子进程。Map 的 key 是任务 ID,value 里保存这条任务自己的命令、状态、时间和输出。

246. 技术:HarnessX 第 13 课:让慢 bash 先进后台,完成后用通知回到 Agent Loop 图表 7

即使命令 B 比命令 A 更早完成,结果也不会串:

  • 命令 A 的回调通过闭包记着 bg_0001,只更新 Map 里的 bg_0001
  • 命令 B 的回调通过闭包记着 bg_0002,只更新 Map 里的 bg_0002
  • 通知里的 task_idcommandstatussummary 都来自同一条 Map 记录。
  • 通知注入后删除对应 ID,避免同一结果被重复通知。

这个 ID 只保证单个 HarnessX 进程内唯一。重新运行 hx agent 后,计数器会从零开始,又可能出现 bg_0001。当前后台任务表也不会跨进程恢复,所以它不是数据库主键,也不承担全局唯一性。

模型也能感知 bg_0001,但它不能直接读取后台任务表。HarnessX 必须先把 ID 写进 messages,再通过下一次 HTTP 请求发给模型。

246. 技术:HarnessX 第 13 课:让慢 bash 先进后台,完成后用通知回到 Agent Loop 图表 8

所以模型的感知分成两个时间点:

  • 启动后:
    • 模型从 role: "tool" 的占位结果里看到 bg_0001
  • 完成后:
    • 模型从 role: "user"<task_notification> 里看到同一个 ID、最终状态和输出。

模型没有后台进程的实时视图。HarnessX 不发起下一轮模型请求,模型就不会知道状态发生了变化。

一次完整的一来一回

下面这张图同时画出模型请求、前台工作和后台完成。

246. 技术:HarnessX 第 13 课:让慢 bash 先进后台,完成后用通知回到 Agent Loop 图表 9

这张图最重要的一段是:后台 sleep 还没完成时,模型已经可以发起 read_file

发给 DeepSeek 的请求和返回

第一次请求里,HarnessX 仍然发送原来的工具列表,只是 bash schema 多了一个布尔参数。

json
{
  "model": "deepseek-chat",
  "messages": [
    {
      "role": "user",
      "content": "后台执行 sleep 1 && echo background done,等待期间读取 README.md 前 20 行"
    }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "bash",
        "parameters": {
          "type": "object",
          "properties": {
            "command": { "type": "string" },
            "run_in_background": { "type": "boolean" }
          },
          "required": ["command"]
        }
      }
    }
  ],
  "tool_choice": "auto"
}

DeepSeek 返回的是结构化工具调用,不会自己启动本机进程。

记住,是模型本身,来判断是否 run_in_background

json
{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "tool_calls": [
          {
            "id": "call_bg_001",
            "type": "function",
            "function": {
              "name": "bash",
              "arguments": "{\"command\":\"sleep 1 && echo background done\",\"run_in_background\":true}"
            }
          }
        ]
      },
      "finish_reason": "tool_calls"
    }
  ]
}

HarnessX 启动子进程后,马上用原来的 tool_call_id 回填一次占位结果。

json
{
  "role": "tool",
  "tool_call_id": "call_bg_001",
  "content": "[Background task bg_0001 started]\nCommand: sleep 1 && echo background done\nResult will be injected as <task_notification> when complete."
}

后台完成后,HarnessX 追加一条新的 role: "user" 消息。

json
{
  "role": "user",
  "content": "<task_notification>\n  <task_id>bg_0001</task_id>\n  <status>completed</status>\n  <command>sleep 1 && echo background done</command>\n  <output_chars>15</output_chars>\n  <summary>background done</summary>\n</task_notification>"
}

这里的事实边界必须分清:

246. 技术:HarnessX 第 13 课:让慢 bash 先进后台,完成后用通知回到 Agent Loop 图表 10

一个 tool_call 只对应一个 role: "tool" 结果。

后台完成发生在未来,它是一个独立事件,所以不能再冒充原始工具结果。当前实现把它放进新的用户侧消息,让模型在下一轮看到。

后台任务的状态怎么变化

后台任务表是进程内状态,不是第 12 课的持久化任务板。

246. 技术:HarnessX 第 13 课:让慢 bash 先进后台,完成后用通知回到 Agent Loop 图表 11

边界也很明确:

  • Agent 进程退出后,内存任务表不会恢复。
  • 当前没有停止任务、查询任务、跨进程恢复。
  • 最终回答出现但后台仍在跑时,主循环最多再等待 5 秒收通知。
  • 这些限制是教学版边界,不在本课继续扩展。

输出太长时怎么处理

后台输出可能比通知本身大很多。全部塞进 messages 会迅速占满上下文。

246. 技术:HarnessX 第 13 课:让慢 bash 先进后台,完成后用通知回到 Agent Loop 图表 12

这和第 8 课的原则一致:大结果留在文件里,上下文只保留继续工作需要的信息。

怎么跑

沿用第 0 课的 npm link 和 DeepSeek 配置。

bash
# 慢命令必须进入后台
# 等待期间还要完成一次前台 read_file
# 最后必须根据后台通知总结,而不是只说“已经启动”
hx agent "启动后台 bash:sleep 2 && echo background done,并设置 run_in_background=true;等待期间调用 read_file 读取 README.md 前 20 行;收到 task_notification 后,总结后台任务的 status、command 和 summary。"

观察这条时间线:

246. 技术:HarnessX 第 13 课:让慢 bash 先进后台,完成后用通知回到 Agent Loop 图表 13

关键日志应当按这个顺序出现:

bash
[BACKGROUND] started bg_0001: sleep 2 && echo background done
> read_file path=README.md limit=20
[BACKGROUND] completed bg_0001: sleep 2 && echo background done
[BACKGROUND] injected notifications=1

判断跑通只看三件事:

  • 先出现后台任务 ID。
  • 后台运行期间出现前台文件读取。
  • 最终回答读到了通知里的真实结果。

这一课真正要记住

  • 工具数量:
    • 参考教程保持 8 个,当前 HarnessX 保持 14 个;S13 都没有新增独立工具。
  • bg_0001
    • 当前进程内唯一的号码牌,也是后台任务 Map 的 key。
  • 占位工具结果:
    • 立即完成原始工具调用的协议配对。
  • <task_notification>
    • 后台完成后的独立事件。
  • 后台任务表:
    • 只在当前 Node.js 进程内保存生命周期。
bash
# 第 13 课一句话
 bash 先拿号码牌,Agent 继续办别的事;叫号后,再把真实结果放回下一轮对话。

源码

源码只保留第 13 课新增链路。先看图,再用 JS 伪码确认它落在什么位置。

源码大流程图

246. 技术:HarnessX 第 13 课:让慢 bash 先进后台,完成后用通知回到 Agent Loop 图表 14

代码概览

工具层只多一个可选参数。

js
// 事实:还是 bash 工具,只增加执行策略参数
bash.parameters = {
  command: "string",
  run_in_background: "boolean?",
};

主循环每轮先收通知,再请求模型。

js
while (还有轮次) {
  messages += 收集已完成的后台通知();
  回复 = await 请求模型(messages, tools);
  messages += 回复;
  messages += await 执行工具调用(回复);
}

代码细分

工具分发先过权限门,再决定是否等待。

js
async function 执行工具调用(调用) {
  if (权限拒绝(调用)) return "Permission denied.";

  if (是后台bash(调用)) {
    return 启动子进程并立即返回任务ID(调用);
  }

  return await 前台执行并等待结果(调用);
}

完成结果作为新事件进入下一轮。

js
function 收集已完成的后台通知() {
  return 后台任务表
    .筛选("completed 或 failed")
    .转成("<task_notification>")
    .从任务表移除();
}