[转][译] 从零开始构建 OpenClaw — 第七部分(子智能体系统)

[转][译] 从零开始构建 OpenClaw --- 第一部分(智能体核心)

[转][译] 从零开始构建 OpenClaw --- 第二部分(技能插件系统)

[转][译] 从零开始构建 OpenClaw --- 第三部分(元技能)

[转][译] 从零开始构建 OpenClaw --- 第四部分(工具循环检测)

[转][译] 从零开始构建 OpenClaw --- 第五部分(对话压缩)

[转][译] 从零开始构建 OpenClaw --- 第六部分(持久化记忆)

[转][译] 从零开始构建 OpenClaw --- 第七部分(子智能体系统)
原文:Building Openclaw from Scratch --- Part 7 (Subagent System)

通过第 1-6 部分,你构建了一个能够调用工具、学习新技能、检测自身失败循环、在上下文满时压缩记忆以及在会话间记住事物的智能体。每一个功能都使单个智能体更加强大。这一部分将完全改变模型。

不再是一个智能体在一个上下文窗口中做所有事情,现在父智能体可以生成子智能体------每个子智能体都有自己隔离的上下文、自己受限的工具集和一个专注的任务。一个智能体进行研究。另一个进行实现。第三个进行审查。每个智能体独立运行,汇报结果,父智能体综合这些结果。

这就是区分一个聪明的聊天机器人和一个 AI 系统的概念:智能体作为可组合的工作单元。

子智能体的重要性------理论

上下文窗口是一个工作区,而不是一个文件柜

你的智能体的上下文窗口中的每个标记都是一块房地产。当你要求你的智能体"研究这个代码库中身份验证的工作方式,然后重构登录模块"时,这两个任务都在争夺同一个空间。智能体在研究期间读取了 30 个文件,用代码片段填满了它的上下文。当它开始重构时,窗口的一半已经被它不再需要的研宄工件占用了。如果上下文满了,压缩功能就会启动,并总结掉它可能仍然需要的细节。

这是根本性的矛盾:宽泛的任务会稀释上下文质量。一个同时进行研究和实现的单一智能体就像一个只有一个显示器的开发者,试图同时阅读文档和编写代码------不断切换窗口,失去位置,忘记细节。

子智能体通过为每个任务提供自己的工作区来解决这个问题。研究智能体用代码分析填充它的上下文。实现智能体以研究摘要加上它自己的工作文件开始,两者互不干扰。

父子架构

子智能体模型是一个树:

js 复制代码
Parent Agent (full tool set, orchestration role)
├── Child Agent A (coding tools only, "research auth patterns")
├── Child Agent B (coding tools only, "document API endpoints")
└── Child Agent C (coding tools only, "review test coverage")

父智能体决定何时进行委派。它在工具集中看到 spawn_subagent ,用特定任务调用它,并将子智能体的完整输出作为工具结果接收。父智能体永远不会失去自己的上下文------子智能体的工作发生在单独的上下文窗口中,只有最终结果会回流。

这是你在进程管理、微服务和团队协调中看到相同模式:一个将任务委派给专家的监督者。

三种防止混乱的约束

无约束的子智能体生成是危险的。一个智能体可能会生成子智能体,子智能体会生成孙智能体,孙智能体会生成曾孙智能体,从而指数级地消耗 API 信用。有三个约束条件使系统保持有限:

  1. 深度限制 --- 子代不能进一步生成子代。在我们的实现中这不是一个计数检查("如果深度 >= 最大值,拒绝"),而是一个结构保证:子智能体简单地不会接收到 spawn_subagent 工具。你不能调用不存在的东西。
  2. 并发限制 --- 一个注册表跟踪活跃的运行。在每次生成之前,系统检查:"已经有 3 个子智能体在运行了吗?如果是,则拒绝。"这可以防止发散爆炸。
  3. 超时清理 --- 每次启动都会开始一个 5 分钟的计时器。如果一个子进程在此时仍未完成,它将通过 AbortController 强制过期,并且其注册条目会被标记为过时。这可以防止僵尸智能体永远阻塞新的启动。

这三个约束 --- 深度、并发、超时 --- 是生产系统中找到的相同原语:进程池、线程池、连接限制。这些概念可以直接迁移。

结果交付模式

当子进程完成时,父进程如何获取结果?有两种常见的模式:

  • 同步(阻塞)--- 父进程的工具调用会阻塞,直到子进程完成。结果作为工具响应直接返回。简单、可预测,无需协调。
  • 异步(基于队列)--- 子进程独立完成。结果被排队,并在父进程准备好时交付。更复杂,但能实现真正的并行。

我们的实现使用同步模式。当父进程调用 spawn_subagent 时,工具的 execute() 函数 await 会等待子会话完成。结果作为正常的工具响应流回。这是最简单的设计,展示了所有概念------注册、生命周期跟踪、超时------而无需消息队列和交付协调的复杂性。

完整的 openclaw 使用异步模式,配合 AnnounceQueue 、重试退避和幂等性键。这是同一想法的生产版本。

实现方案

文件结构

该系统位于 src/subagent/ 下的五个文件中:

js 复制代码
src/subagent/
├── types.ts       (34 lines)  --- Interfaces and config defaults
├── registry.ts    (104 lines) --- In-memory registry, concurrency gating, stale cleanup
├── prompt.ts      (20 lines)  --- Focused system prompt for child agents
├── spawn.ts       (180 lines) --- Child session creation and execution
└── tool.ts        (77 lines)  --- spawn_subagent tool definition

增加约 50 行集成代码在 entry.ts 和 1 行代码在 system-prompt.ts

类型和配置

src/subagent/types.ts 定义了运行记录和默认值:

js 复制代码
export interface SubagentRun {
  id: string;
  task: string;
  label: string;
  status: "running" | "completed" | "failed" | "expired";
  startedAt: number;
  endedAt?: number;
  result?: string;
  error?: string;
  toolsUsed?: string[];
}
export const DEFAULT_CONFIG: SubagentConfig = {
  maxConcurrent: 3,
  maxDepth: 1,
  timeoutMs: 5 * 60_000,
};

SubagentRun 是跟踪的单元。每个生成的子进程都会得到一个。四个状态映射到一个干净的生命周期: runningcompleted | failed | expired

Registry --- 并发门控和过期清理

src/subagent/registry.ts 是一个记忆中的 Map<string, SubagentRun> ,有三个职责:

js 复制代码
export class SubagentRegistry {
  private runs = new Map<string, SubagentRun>();
  private config: SubagentConfig;

canSpawn(): { ok: boolean; reason?: string } {
    this.cleanupStale();
    const active = this.getActive();
    if (active.length >= this.config.maxConcurrent) {
      return {
        ok: false,
        reason: `Max ${this.config.maxConcurrent} concurrent subagents. ${active.length} currently running...`,
      };
    }
    return { ok: true };
  }

cleanupStale() 在并发检查之前运行。这是防御性的------如果一个子进程超时但其中止信号没有干净地触发,过时的清理操作可以防止它永远阻塞新的启动。清理操作会扫描所有运行,找到任何带有 status === "running" 且超时的运行,并强制将它们标记为 "expired"

注册表还提供 complete()fail()getActive()getAll()reset() ------ 每个运行的完整生命周期。

Spawner ------ 会话的诞生

src/subagent/spawn.ts 是系统的核心。 spawnSubagent() 函数创建并运行一个子会话:

js 复制代码
export async function spawnSubagent(
  ctx: SpawnContext,
  task: string,
  label?: string,
): Promise<SpawnResult> {
  // 1. Pre-flight checks
  const check = ctx.registry.canSpawn();
  if (!check.ok) {
    throw new Error(check.reason);
  }

// 2. Register the run
  const id = crypto.randomUUID();
  ctx.registry.register({
    id, task,
    label: label || task.slice(0, 40) + (task.length > 40 ? "..." : ""),
    status: "running",
    startedAt: Date.now(),
  });
  // 3. Set up timeout
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);

预飞行检查控制入口。注册立即发生------即使会话创建失败,运行也会被跟踪。 AbortController 开始一个硬倒计时。

然后创建子会话:

js 复制代码
// Restricted tools: coding tools only (read, write, edit, bash)
    // No spawn_subagent --- this is how we enforce max depth = 1
    const childTools = createCodingTools(ctx.workspaceDir);

    const { session: childSession } = await createAgentSession({
      cwd: ctx.workspaceDir,
      agentDir: ctx.agentDir,
      authStorage: ctx.authStorage,
      modelRegistry: ctx.modelRegistry,
      model: ctx.model,
      tools: childTools,
      customTools: [],
      sessionManager: SessionManager.inMemory(),
      settingsManager,
    });

两个关键选择在这里:

  1. customTools: [] --- 子进程获得零个自定义工具。没有 spawn_subagent ,没有 web_search ,没有 memory_search 。只有编码原语: readwriteeditbash 。这是结构深度限制------你不能调用工具集中不存在的功能。
  2. SessionManager.inMemory() --- 子进程的会话仅存在于记忆中。没有磁盘持久化,没有会话文件。当子进程销毁时,其状态会消失。这对短暂的工人来说完全正确。

任务运行时具有中止处理:

js 复制代码
const onAbort = () => session?.abort();
    controller.signal.addEventListener("abort", onAbort, { once: true });
    await session.prompt(task);
    controller.signal.removeEventListener("abort", onAbort);

如果 5 分钟定时器触发,中止信号会触发 session.abort() ,这会中断子进程的 LLM 循环。 finally 块始终进行清理:

js 复制代码
} finally {
    clearTimeout(timer);
    if (session) {
      try { session.dispose(); } catch { /* ignore */ }
    }
  }

计时器已清除,会话已释放。无论成功、失败还是超时,都没有泄漏的资源。

系统提示词 --- 设计简洁

src/subagent/prompt.ts 为子智能体构建一个专注的提示词:

js 复制代码
export function buildSubagentPrompt(workspaceDir: string): string {
  return [
    "You are a subagent --- a focused assistant spawned to complete a specific task.",
    "",
    "Rules:",
    "- Complete the assigned task thoroughly, then report your findings clearly.",
    "- You have coding tools (read, write, edit, bash) but cannot spawn further subagents.",
    "- Be concise --- your full output is reported back to the parent agent.",
    "- Focus only on the assigned task. Do not ask follow-up questions.",
    "",
    `Workspace: ${workspaceDir}`,
  ].join("\n");
}

这是有意为之的简短内容。父智能体的系统提示词包含数百个标记------技能目录、记忆内容、项目上下文文件、工具摘要。子智能体不需要任何这些内容。它的全部工作就是:完成任务,并汇报结果。系统提示词中每节省一个标记,就有一个标记可用于实际工作。

工具定义------LLM 所看到的内容

src/subagent/tool.ts 将 spawner 暴露为 LLM 可调用的工具:

js 复制代码
export function createSubagentToolDefinition(ctx: SpawnContext) {
  return {
    name: "spawn_subagent",
    description: [
      "Delegate a task to a subagent with its own isolated context window.",
      "The subagent runs to completion and returns its result.",
      "Use for tasks that benefit from a fresh context: research, file analysis, focused implementation.",
      `Max ${ctx.config.maxConcurrent} concurrent subagents. Subagents cannot spawn further subagents.`,
    ].join(" "),
    parameters: {
      type: "object",
      properties: {
        task: { type: "string", description: "The task for the subagent to complete." },
        label: { type: "string", description: "Short label for tracking this subagent." },
      },
      required: ["task"],
    },

execute 函数优雅地捕获错误,将其作为内容返回而不是抛出:

js 复制代码
try {
        const result = await spawnSubagent(ctx, task, label);
        const header = [
          `Subagent completed in ${(result.durationMs / 1000).toFixed(1)}s`,
          result.toolsUsed.length > 0 ? `Tools used: ${result.toolsUsed.join(", ")}` : null,
        ].filter(Boolean).join(" | ");
        return {
          content: [{ type: "text", text: `[${header}]\n\n${result.result}` }],
        };
      } catch (err: any) {
        return {
          content: [{ type: "text", text: `Subagent error: ${err.message}` }],
        };
      }

这是重要的------父智能体将错误视为文本,而不是崩溃。它可以决定如何响应:使用不同的任务重试,回退到自己执行工作,或向用户报告失败。

与 entry.ts 集成

父智能体在启动时创建注册表和生成上下文:

js 复制代码
const subagentRegistry = new SubagentRegistry();
const spawnContext: SpawnContext = {
  workspaceDir, agentDir: AGENT_DIR,
  authStorage, modelRegistry, model,
  registry: subagentRegistry,
  config: SUBAGENT_DEFAULT_CONFIG,
};
const customTools = buildCustomTools(spawnContext);

/agents 命令显示运行历史:

js 复制代码
if (trimmed === "/agents") {
  const runs = subagentRegistry.getAll();
  for (const run of runs) {
    const icon = run.status === "completed" ? "✓"
      : run.status === "failed" ? "✗"
      : run.status === "expired" ? "⏱"
      : "⏳";
    console.log(`  ${icon} ${run.label} (${duration}) ${preview}`);
  }
}

并且注册表在 /new 时与循环检测器一起重置:

js 复制代码
loopDetector.reset();
subagentRegistry.reset();

尝试使用

js 复制代码
┌ openclaw-mini
│ model: anthropic/claude-sonnet-4-20250514
│ workspace: /Users/you/project
│ tools: read, bash, edit, write, web_fetch, web_search, memory_search, spawn_subagent
└ /new /think /model /skills /verbose /compact /memory /agents /quit

> use a subagent to find all TODO comments in this project and summarize them
[tools: spawn_subagent]
[Subagent completed in 6.2s | Tools used: bash, read]
The subagent found 12 TODO comments across 5 files:
- src/entry.ts:142 - TODO: add model switching support
- src/tools/web-fetch.ts:38 - TODO: handle timeout errors
...
(7.0s)
> /agents
Subagent runs (1):
  ✓ find all TODO comments in this project... (6.2s) The subagent found 12 TODO comments acro...

父智能体选择将 grep 和分析任务委托给子智能体。子智能体在其自己的上下文中运行 bashread ,生成摘要,结果作为工具响应返回。父智能体自己的上下文几乎未被触及------只是工具调用及其响应。

你构建了什么

  • 子智能体生成------父智能体将任务委托给具有隔离记忆会话的子智能体(跨越 5 个文件,约 430 行代码)
  • 三个安全约束------并发限制(3)、结构深度限制(子代无法获得生成工具)和超时清理(5 分钟)
  • 基于注册表的生命周期 --- 每次运行从创建到完成/失败/过期都被跟踪,并在每次生成尝试中进行陈旧清理
  • /agents 命令 --- 查看哪些子智能体已运行此会话

下一步是什么

自然演化是工作流编排------链式模式,其中每个智能体的输出为下一个提供输入(侦察兵→规划器→工作者),以及并行模式,用于同时跨多个专注智能体进行分支。随着注册表和生成器的准备就绪,这些是建立在您刚刚构建的相同基础上的执行模式。

相关推荐
an317423 小时前
解决 VSCode 中 ESLint 格式化不生效问题:新手也能看懂的配置指南
前端·javascript·vue.js
Lee川5 小时前
🚀《JavaScript 灵魂深处:从 V8 引擎的“双轨并行”看执行上下文的演进之路》
javascript·面试
比特鹰5 小时前
手把手带你用Flutter手搓人生K线
前端·javascript·flutter
大雨还洅下5 小时前
前端JS: 数组扁平化
javascript
奔跑路上的Me5 小时前
前端导出 Word/Excel/PDF 文件
前端·javascript
bluceli5 小时前
JavaScript异步编程深度解析:从回调到Async Await的演进之路
前端·javascript
SuperEugene5 小时前
路由与布局骨架篇:布局系统 | 头部、侧边栏、内容区、面包屑的拆分与复用
前端·javascript·vue.js
代码煮茶5 小时前
前端网络请求实战 | Axios 从入门到封装(拦截器 / 错误处理 / 重试)
javascript
进击的尘埃5 小时前
组合式函数 Composables 的设计模式:如何写出可复用的 Vue3 Hooks
javascript