前两篇覆盖了单个 Agent 从接收消息到回复的完整链路,以及记忆系统的三层架构。这一篇聚焦多 Agent 协作:主 Agent 如何拆分任务、子 Agent 如何独立执行、结果如何汇总回来。
我:主 Agent 是怎么创建子 Agent 的?
严格来说,主 Agent 不能"创建"任何东西。它只能返回一段 JSON 表达意图:
{
"type": "tool_use",
"name": "sessions_spawn",
"input": {
"task": "把这个 Python 脚本转成 TypeScript",
"model": "sonnet",
"label": "refactor-job"
}
}
真正创建子 Agent 的是 Gateway。它解析这段 JSON,生成新的 session key,入队到 subagent 队列,调用和主 Agent 完全相同的 runEmbeddedPiAgent() 函数。子 Agent 的 session 是全新的——没有主 Agent 的对话历史,只有 task 描述作为第一条消息。系统提示也是精简版,省掉了人格、用户档案、心跳规则这些跟任务无关的内容。
主 Agent 和子 Agent 的差异:
主 Agent 子 Agent
System Prompt 完整版(full) 精简版(minimal)
注入的文件 全部 6 个 .md 只有 AGENTS.md + TOOLS.md
对话历史 完整的频道历史 只有 task 描述
模型 可以不同 可以指定不同模型
结果去向 发给用户 推送回主 Agent 会话
队列 lane main(并发 4) subagent(并发 8)
这个设计揭示了一个容易误解的事实:模型没有执行能力。所有工具——读文件、跑命令、发消息、创建子 Agent——都是模型返回 JSON,Gateway 执行。模型是大脑,Gateway 是身体。
我:主 Agent 和子 Agent 之间怎么通信?
三种通道,都经过 Gateway 中转:
sequenceDiagram
actor User as 用户
participant MA as 主 Agent
participant GW as Gateway
participant SA as 子 Agent
User->>MA: "帮我做三件事"
MA->>GW: tool_use: sessions_spawn
Note over GW: 创建新 session<br/>入队 subagent lane
GW->>SA: task 描述(唯一初始输入)
MA-->>User: "已经在后台处理了"
Note over User,MA: 用户可以继续跟主 Agent 聊别的
Note over SA: ReAct 循环执行中...
opt 中途干预(可选)
MA->>GW: subagents steer
GW->>SA: 注入新消息
Note over SA: 下一个工具边界处读到<br/>调整方向
end
SA->>GW: end_turn(任务完成)
Note over GW: 提取子 Agent 输出<br/>构造系统消息
GW->>MA: [System Message] 子任务完成:结果...
Note over MA: 被动唤醒<br/>新一轮 ReAct 循环
MA-->>User: "任务完成了,结果是..."
启动时,主 Agent 通过 sessions_spawn 传递 task 描述,这是子 Agent 收到的唯一初始输入。运行中,主 Agent 可以通过 subagents steer 注入新指令,子 Agent 在下一个工具边界处读到它。完成时,子 Agent 的结果由 Gateway 自动以系统消息注入回主 Agent 的会话。
这套通信是单向推送的。子 Agent 不能主动联系主 Agent,也不能和其他子 Agent 通信。如果任务之间有依赖关系,不适合并行 spawn,应该在主 Agent 里串行处理。
我:系统消息是什么?为什么子 Agent 的结果要包装成系统消息?
系统消息是 OpenClaw 内部的通信机制。LLM API 只有三种 role:system(系统提示)、user(用户)、assistant(模型)。Gateway 没有其他方式把消息塞进对话,只能伪装成 user message,但用 [System Message] 前缀标识。
{
"role": "user",
"content": "[System Message] Sub-agent \"refactor-job\" completed:\nTypeScript 转换完成,改了 3 个文件..."
}
模型的系统提示里有明确规则:系统消息是内部上下文,用户在聊天界面看不到;如果系统消息报告了任务完成,应该用自己的话转述给用户,不要原样转发。
产生系统消息的场景不只是子 Agent 完成。Cron 定时任务触发、Discord reaction 通知、心跳轮询、用户在处理过程中发新消息(steering)——这些都会以系统消息的形式注入会话。它本质上是 Gateway 和模型之间的"内部通信频道",借用了 user message 的壳。
我:主 Agent end_turn 后 Gateway 把消息发给用户,子 Agent end_turn 后 Gateway 把消息发给主 Agent——是这个逻辑吗?
完全正确。而且子 Agent 的结果注入主 Agent 会话后,会自动触发主 Agent 的一次新模型调用——不是轮询,是被动唤醒。系统消息进来就像用户发了条新消息一样,直接启动一轮新的 ReAct 循环。主 Agent 看到结果后决定要不要转述给用户。如果不需要打扰用户,它也可以回复 NO_REPLY 保持沉默。
graph LR
subgraph "主 Agent"
MA_END["end_turn"]
end
subgraph "子 Agent"
SA_END["end_turn"]
end
MA_END -->|"Gateway 路由"| USER["用户(Discord/Telegram/...)"]
SA_END -->|"Gateway 包装为系统消息"| MA_SESSION["主 Agent 会话"]
MA_SESSION -->|"触发新一轮 ReAct"| MA_END
我:NO_REPLY 一定是 Agent 自己决定的吗?
两种情况都有。模型可以主动回复 NO_REPLY 表示"没什么要说的"。Gateway 也会在发送前做过滤:
graph TD
A["LLM 返回 end_turn"] --> B{"文本里有 NO_REPLY?"}
B -->|"有"| C["过滤掉"]
B -->|"没有"| D{"message 工具已经发过?"}
D -->|"是"| E["去重,移除重复确认"]
D -->|"否"| F{"过滤后还有内容?"}
E --> F
C --> F
F -->|"有"| G["发到 Discord"]
F -->|"没有"| H{"有工具错误?"}
H -->|"有且没发过回复"| I["发 fallback 错误消息"]
H -->|"否"| J["静默,不发送"]
模型的自觉是第一层,Gateway 的过滤是第二层,双重保障。
我:Gateway 怎么同时管理主 Agent 和多个子 Agent?
它们跑在同一个 Node.js 进程里,用的是同一套异步函数调用和事件流机制。没有网络通信,没有 RPC,没有消息队列中间件。Gateway 调用 runEmbeddedPiAgent() 后,通过事件订阅接收模型的流式输出、工具调用状态、生命周期事件。主 Agent 和子 Agent 的唯一区别是排队的 lane 不同。
graph TB
GW["Gateway(单个 Node.js 进程)"]
GW --> EL["Event Loop"]
GW --> Q["Promise 队列"]
GW --> FS["文件系统"]
subgraph "运行时状态(进程内)"
EL
Q
end
subgraph "持久化状态(磁盘)"
FS --> J["会话历史:JSONL"]
FS --> C["配置:JSON"]
FS --> MD["记忆:Markdown"]
FS --> SQ["向量索引:SQLite"]
end
整个 OpenClaw 系统没有数据库。会话历史是 JSONL 文件,配置是 JSON 文件,记忆是 Markdown 文件,向量索引是 SQLite。所有状态都在文件系统上。备份就是复制目录,迁移就是搬文件,调试就是打开文件看。
这是一个为个人助手场景高度简化的架构。代价是不支持分布式部署和多实例。但对于"一个人的 AI 助手"这个定位,它做到了足够的简洁和可靠。