# OpenClaw 场景切换体系：282 Agents 动态管理，告别 100GB 内存爆炸


---
# OpenClaw 场景切换体系：282 Agents 动态管理，告别 100GB 内存爆炸


## 一、噩梦的开始：100GB 内存的「死机」之夜

深夜两点，手机震动——服务器告警。Gateway 进程已吃掉 100GB+ 内存，Swap 爆满，OOM Killer 正在游荡。

`htop` 画面上，OpenClaw Gateway 进程像一个黑洞，吞噬着服务器的每一滴内存。Restart 之后不出 30 分钟，同样的场景再次上演。

**为什么？**

因为我的 `openclaw.json` 里定义了整整 **282 个 Agents**。

不是手写的——手写 282 个 Agent 配置会写到天荒地老。它们来自 blog-writer skill 编排器自动生成的工作流 Agent、各种第三方 skill 的入口 Agent、定制化的工具 Agent……每一个都乖乖躺在配置里，等着被 Gateway 加载到内存。

Gateway 的行为很简单：**启动时一次性加载所有 Agent 配置到内存**。282 个 Agent 的完整配置树、system prompt、tool 描述、路由信息……加在一起，内存占用轻松突破 100GB。

关键是，**这 282 个 Agent 我根本不会同时使用**。我在写作时只需要 20 个左右，做自媒体运营时需要 50 个左右。其他 200 多个 Agent 完全是「占着茅坑不拉屎」——它们躺在内存里，除了给 OOM Killer 送业绩之外毫无贡献。

这就是场景切换体系要解决的核心问题：**按需加载，动态切换，而不是一次全部拉满**。

## 二、设计原则：一份无处逃逸的配置源

在动手之前，先确立几条铁律：

### 2.1 openclaw.json.full 是唯一真相

**所有 Agent 配置的权威来源只有一个文件：`openclaw.json.full`。**

这个文件包含全部 282 个 Agent 的完整配置，是手写维护的唯一目标。任何时候有人问"这个 Agent 的配置是哪来的"，答案都是「从 `openclaw.json.full` 里来的」。

这意味着：

- **永不直接编辑 `openclaw.json`**——它只是一个「运行时产物」，随时可以被覆盖
- **任何场景切换工具都只读 `openclaw.json.full`，从它出发**
- **`openclaw.json` 的本质是一个「缓存」，不是「源码」**

### 2.2 场景 = 预定义的 Agent 子集

每个工作场景是一个 JSON 文件，定义了这个场景需要加载哪些 Agents，以及对应的 Gateway 配置参数。

三个初始场景：

| 场景 | Agents 数量 | 适用场景 |
|------|------------|---------|
| `writing` | ~21 | 博客写作、知识库归档 |
| `social` | ~50 | 自媒体运营（小红书、抖音、B站） |
| `full` | ~282 | 完整开发测试 |

切换场景的实质：根据场景定义，从 `openclaw.json.full` 里抽取出对应 Agent 子集，生成新的 `openclaw.json`，然后重启 Gateway。

### 2.3 保持事变链可追溯

每次切换时记录时间戳、源 commit、场景名，写入日志。这样哪天出问题了，可以回溯「是谁在什么时候切换到了什么场景」。

## 三、switch-scene.py：统一入口的设计

所有场景切换操作通过一个 Python 脚本完成：

```bash
python3 switch-scene.py writing    # 切换到写作模式
python3 switch-scene.py social     # 切换到自媒体模式
python3 switch-scene.py full       # 切换到完整模式
python3 switch-scene.py list       # 查看当前场景
python3 switch-scene.py status     # 查看详细状态
```

`switch-scene.py` 本身是一个有状态的 CLI——它维护了一个简化的状态文件 `.scene_state.json`，记录当前场景和时间戳。

**核心逻辑大致如下：**

```
1. 读取场景定义文件（scenes/writing.json 或 scenes/social.json 等）
2. 从 openclaw.json.full 中按 agent_id 列表过滤出该场景的 Agent 子集
3. 合并场景级别的 Gateway 配置覆盖（如 --memory-budget 等）
4. 写入 openclaw.json
5. 记录切换日志
6. 提示重启 Gateway
```

**关键设计细节之——Agent 完整性检查：**

过滤出来的 Agent 子集，每个 Agent 会检查是否能在 `openclaw.json.full` 中找到完整的定义。如果场景配置里声明了一个不存在的 Agent，脚本会退出并报错，而不是生成一个残缺的 `openclaw.json`。

```bash
# 示例：检查 writing 场景需要的 Agent 是否都在 full 中
python3 switch-scene.py check writing
```

**场景定义文件示例**（`scenes/writing.json`）：

```json
{
  "scene": "writing",
  "description": "写作场景：博客创作 + 知识库管理",
  "agents": [
    "blog-writer",
    "kb-writer",
    "kb-adapter",
    "note-taker",
    "outline-editor",
    "content-reviewer",
    ...
  ],
  "gateway_overrides": {
    "max_memory_mb": 4096
  }
}
```

## 四、update-full：纯文本级合并

这是整个体系中最大的踩坑点。

### 4.1 最初的想法（失败）

最初的想法是「每次往 `openclaw.json.full` 里加 Agent 时，写一个 API 调用来更新 JSON」。但很快发现这条路走不通——JSON 的层次结构太复杂了：

- 有些 Agent 需要嵌套在 parent 下面
- 有些需要更新某个 Agent 的 tool list
- 有些需要修改 model 配置

写一个通用的 JSON 合并 API，光处理各种 edge case 就足够写 500 行代码，而且还容易出 bug。

### 4.2 最终的方案：纯文本级合并

最后选择了最粗暴也最可靠的方案：**纯文本字符串替换**。

```bash
python3 update-full.py --agent-table agent_table.json
```

`update-full.py` 做了什么？

1. 读取 `agent_table.json`（一个扁平化的 Agent 定义表）
2. 从 `openclaw.json.full` 中找到对应的插入位置（通过正则匹配特定的锚点注释）
3. 直接做字符串替换

这个方案的好处：
- **零 JSON 解析风险**：不反序列化再序列化，不会出现格式重排、顺序打乱等问题
- **保留注释和格式**：`openclaw.json.full` 里我维持了大量注释，纯文本处理不会丢失它们
- **幂等**：多次运行相同输入，结果一致

锚点注释的设计：

```json
"agents": {
  // ##AGENT-BLOCK-START## blog-writer
  "agent:blog-writer": {
    "name": "blog-writer",
    ...
  },
  // ##AGENT-BLOCK-END## blog-writer
}
```

`update-full` 通过 `##AGENT-BLOCK-START##` 和 `##AGENT-BLOCK-END##` 这种锚点来定位替换区域。

### 4.3 update-full 使用流程

```bash
# 1. 创建或修改 agent_table.json（新增一个 Agent 定义）
# 2. 执行合并
python3 update-full.py --agent-table agent_table.json

# 3. 验证没有坏掉整个 JSON
python3 -c "import json; json.load(open('openclaw.json.full')); print('Valid!')"

# 4. 提交到版本管理
git add openclaw.json.full agent_table.json
git commit -m "feat: 新增 xxx Agent"
```

## 五、升级流程：安全的变更管理

有了场景切换和配置合并，接下来需要一套安全的升级流程。

### 5.1 日常增删 Agent

```mermaid
graph TD
    A[修改 agent_table.json] --> B[运行 update-full.py]
    B --> C[验证 JSON 合法性]
    C --> D[Git commit]
    D --> E[switch-scene.py 切换到目标场景]
    E --> F[重启 Gateway]
```

### 5.2 场景配置变更

```bash
# 修改 scenes/writing.json 里的 agent list
# 切换验证
python3 switch-scene.py writing --dry-run

# 实际切换
python3 switch-scene.py writing
```

### 5.3 紧急回滚

```bash
# 回退到上一个版本
git checkout HEAD~1 -- openclaw.json.full
python3 switch-scene.py full
```

### 5.4 GC/backup 场景的独立配置

Gateway 的 `openclaw.json` 用 `switch-scene` 来管理，但有些运维任务是独立运行的——比如 GC cron job 和 backup 脚本。这些任务有自己的独立配置，不用 `switch-scene` 管理，直接在 cron 里写死。

```bash
# GC清理（独立运行）
0 3 * * * /opt/openclaw/scripts/gc-cleanup.sh

# 全量备份（独立运行）
0 4 * * * /opt/openclaw/scripts/backup-full.sh
```

## 六、Blog-Writer Skill 编排器设计

场景切换体系搭好之后，所有工作流都跑在它上面。其中最重要的就是 blog-writer skill 的编排器（orchestrator）。

### 6.1 编排器 vs. 单一 Agent

为什么不把所有逻辑塞到一个 Agent 里？

答案很简单：**单一 Agent 的 token 窗口和 context 长度有限**。写一篇文章可能需要 5 个步骤（调研、列大纲、写初稿、改写、审校），如果放在一个 Agent 里，前几步的上下文会污染后几步。

编排器的思路是：**把一个大任务拆成多个小 Agent，每个 Agent 负责一个子任务，编排器负责调度和上下文传递**。

### 6.2 三个阶段

blog-writer skill 的编排分为三个阶段：

**阶段一：从 openclaw.json.full 生成写作场景 Agent**

```bash
python3 switch-scene.py writing
```

这个命令从 full 配置中提取约 21 个写作相关 Agent，包括：
- `research-agent`：研究方向调研
- `outline-agent`：文章大纲生成
- `content-agent`：正文写作（一级）
- `rewrite-agent`：改写润色（二级）
- `review-agent`：审校（三级）
- `kb-adapter`：知识库适配器

**阶段二：编排器分发写作任务**

编排器接收用户的写作指令（主题、字数、风格），自动编排写作流程——按顺序调用各 Agent，传递中间结果。这个编排器本身也是一个 Agent，定义在 `openclaw.json.full` 中。

**阶段三：归档和销毁**

写作完成后，编排器触发 kb-writer Agent 将整篇文章归档到知识库，然后释放资源。

### 6.3 为什么是三级审核

文章的质量控制设计了三关：

1. 一级（content-agent）：写初稿，重点在框架完整、论证充分
2. 二级（rewrite-agent）：改写优化，重点在表达流畅、结构合理
3. 三级（review-agent）：最终审校，重点在逻辑一致性、事实准确性

三级审核不是过度设计——在写较长的技术文章时，每个 Agent 的注意力集中在自己的职责上，比一个 Agent 写到底的质量要高得多。

## 七、踩坑记录：那些让我通宵的 Bug

### 7.1 Bug#1：纯文本合并的锚点偏移

**现象**：运行 `update-full.py` 后，`openclaw.json.full` 的某些区域出现重复或缺失。

**原因**：锚点注释的定位是「字符串精确匹配」。当修改 agent_table.json 后重新运行合并时，如果老锚点已经被修改过，正则匹配到的位置不对，导致内容插入到错误的地方。

**修复**：给锚点加了唯一性哈希后缀，确保即使内容变化，锚点也能被精确识别。

```json
// ##AGENT-BLOCK-START## blog-writer##7f3a1c##
```

**教训**：纯文本合并虽然简单，但锚点的健壮性是命门。不要相信任何「自然语言」级别的锚点——必须用可验证的标识符。

### 7.2 Bug#2：Gateway 增量加载导致乱序

**现象**：从 writing 场景切换到 social 场景后，Gateway 正常。但如果回头切回 writing，部分 Agent 的路由出现混乱。

**原因**：Gateway 的 reload 机制是增量加载——它不会清空已加载的 Agent 列表，而是把新的配置整合到现有状态上。切换场景时如果某些 Agent 在旧场景存在但在新场景不存在，它们的路由信息仍然留在 Gateway 内存中。

**修复**：`switch-scene.py` 在切换前发送 `--reload --no-incremental` 参数给 Gateway，强制全量重载。

```python
# switch-scene.py 关键逻辑
subprocess.run([
    "./openclaw", "gateway", "reload",
    "--config", "openclaw.json",
    "--no-incremental"
])
```

### 7.3 Bug#3：场景切换导致正在运行的 job 中断

**现象**：blog-writer 正在写文章，运维人员切换到 social 场景，Gateway 重启，正在生成的文章直接丢失。

**原因**：`switch-scene` 没有检查是否有活跃的 Agent 任务。

**修复**：切换前检查是否有正在运行的任务：

```bash
python3 switch-scene.py check-active
# 如果有活跃任务，拒绝切换并提示
```

添加了 `--force` 参数允许强制切换：

```bash
python3 switch-scene.py social --force  # 强制切换，中断正在运行的任务
```

### 7.4 Bug#4：`openclaw.json.full` 格式意外被 JSON 格式化器破坏

**现象**：发现 `openclaw.json.full` 里所有的注释和格式化缩进被清除了。

**原因**：某个脚本里调用了 `json.dumps` 来写 `openclaw.json.full`，而在 Python 中 `json.dumps` 默认会丢弃所有注释（JSON 标准不支持注释）。一次 git merge 失误触发了一个格式化步骤。

**修复**：在 `.git/hooks/pre-commit` 中增加了检查：

```bash
#!/bin/bash
# 防止 openclaw.json.full 被 JSON 格式化器破坏
if git diff --cached --name-only | grep -q 'openclaw.json.full'; then
    echo "Warning: openclaw.json.full is in staging. Running sanity check..."
    python3 -c "
import json
with open('openclaw.json.full') as f:
    content = f.read()
# 检查是否有注释被移除
if '//' not in content:
    print('ERROR: Comments have been stripped from openclaw.json.full!')
    print('Commit blocked. Do NOT use json.dumps on this file.')
    exit(1)
"
    if [ $? -ne 0 ]; then
        exit 1
    fi
fi
```

## 八、当前状态与成果

经过两轮迭代（初始版 + 踩坑修复），场景切换体系已经平稳运行：

| 指标 | 优化前 | 优化后 |
|------|--------|--------|
| 内存占用 | 100GB+（死机） | 4GB（writing）/ 8GB（social）/ 30GB（full） |
| 切换耗时 | N/A（从未成功） | ~8秒（包含 Gateway reload） |
| Agent 管理 | 手动编辑 JSON | agent_table + update-full |
| 可追溯性 | 无 | 每次切换有日志 |

更重要的是，这个体系让整个 OpenClaw 实例变得可维护了。新加一个 Agent 不再需要「祈祷」——知道会有多少人受到影响。日常工作时只加载需要的 Agent，开发测试时才上 full 模式。

## 九、如果你也要做类似的事

有几个建议送给面临同样问题的朋友：

1. **不要一次性加载不需要的 Agent**——这是最直接的性能优化。Gateway 的内存占用 ≈ 所有 Agent 配置树大小的总和。Agent 数量翻倍，内存几乎翻倍。

2. **用纯文本处理 JSON 配置**——看似不优雅，但实际上是最可靠的方式。JSON 的序列化/反序列化每次都会改变格式，而字符串替换不会。只要锚点设计得好，纯文本合并比 JSON API 稳定 10 倍。

3. **场景定义文件要版本管理**——把 `scenes/` 目录纳入 git 管理，每次场景配置变更都有历史记录。某天切错了场景，能快速回溯「之前的 writing 场景是啥样的」。

4. **预留 --dry-run 模式**——切换场景前先 dry-run，很多人不敢用 `switch-scene.py` 就是因为怕把生产环境搞坏。dry-run 只生成 `openclaw.json` 但不重启 Gateway，方便人工 review。

5. **Gateway 配置文件和场景切换体系要解耦**——GC、backup、监控这些运维任务不依赖场景管理，它们有自己的独立配置。场景切换只影响 Agent 负载，不影响基础设施。

## 十、未来的方向

场景切换体系目前还算够用，但有几个可以改进的方向：

1. **热加载**——目前切换后需要 reload Gateway（~8秒），更理想的是 Agent 级别的热加载，不需要中断其他 Agent 的工作流

2. **场景自动切换**——根据当前用户的任务类型自动检测并切换场景，不需要运维手动执行

3. **内存预算管理**——每个场景都有内存预算上限，当某个 Agent 的 task 超出预算时自动杀死，并回退到最小配置

4. **更智能的 update-full**——目前基于锚点替换的机制虽然稳定，但 agent_table 和 full 之间的 diff 展示不够友好，可以考虑集成 `json-diff` 工具

---

如果你也在管理大量 OpenClaw Agents，欢迎交流踩坑经验。这个场景切换体系虽然不完美，但至少让我从「半夜被 OOM 告警叫醒」的状态中解脱出来了。

***
--全文完--

<a href="/images/Journal-Notes/Thank-you-for-reading-1600.webp" class="lightgallery">
    <img src="/images/Journal-Notes/Thank-you-for-reading-1200.webp"
         srcset="/images/Journal-Notes/Thank-you-for-reading-800.webp 800w,
                 /images/Journal-Notes/Thank-you-for-reading-1200.webp 1200w,
                 /images/Journal-Notes/Thank-you-for-reading-1600.webp 1600w"
         sizes="(max-width: 600px) 800px, (max-width: 1200px) 1200px, 1600px"
         alt="感谢阅读"
         loading="lazy"
         style="width:100%; height:auto; border-radius:8px;">
</a>

***


{{< admonition type=question title="若你有故事想讲、有困惑想聊、或是想找个人说说心里话，甚至只是吐槽发泄一下情绪，都欢迎来找我聊聊：　　　《内容已折叠，点击展开》 " open=false >}}

{{< typeit tag=h4 >}}

**"同频之人，终会相遇；同行之路，终有光亮。愿与身处同境、灵魂同频、砥砺前行的你相遇相伴，倾听彼此的故事与困惑，分享心路与感悟，在逆境中自救破局的路上彼此陪伴、相互照亮、同行向前,一起走向重生与新生。"**...

{{< /typeit >}}

<figure style="width:100%; margin:0;">
  <a href="https://www.oklife.me/about/"
     style="display:block; transition:opacity .25s ease;">
    <img src="/images/site-wide/about-me-1200.webp"
         srcset="/images/site-wide/about-me-800.webp 800w,
                 /images/site-wide/about-me-1200.webp 1200w,
                 /images/site-wide/about-me-1600.webp 1600w"
         sizes="(max-width: 600px) 800px, (max-width: 1200px) 1200px, 1600px"
         alt="关于我页面配图"
         loading="lazy"
         style="width:100%; height:auto; border-radius:8px; display:block;">
  </a>

  <figcaption style="text-align:center; margin-top:8px; color:#666; font-size:14px;">
    <a href="https://www.oklife.me/about/"
       style="color:inherit; text-decoration:none;">
      点击跳转：关于我
    </a>
  </figcaption>
</figure>


{{< /admonition >}}



***
{{< admonition type=success title="希望我写的每一个字，成为我自己和某个人活下去、拼下去的力量。　　　　　　　　　　　　　　　　　　　　　《内容已折叠，点击展开》" open=false >}}
"技术终归是工具，而我们一次次认真把问题理顺，守住的其实不只是页面样式和代码输出，还有那一点不愿被混乱打败的心气，是每一个深夜仍愿点灯前行的人。"

转载请注明来自https://oklife.me。

<a href="/images/post-end/night-rain-lamp-1600.webp" class="lightgallery">
    <img src="/images/post-end/night-rain-lamp-1200.webp"
         srcset="/images/post-end/night-rain-lamp-800.webp 800w,
                 /images/post-end/night-rain-lamp-1200.webp 1200w,
                 /images/post-end/night-rain-lamp-1600.webp 1600w"
         sizes="(max-width: 600px) 800px, (max-width: 1200px) 1200px, 1600px"
         alt="文尾配图水墨画图片"
         loading="lazy"
         style="width:100%; height:auto; border-radius:8px;">
</a>
{{< /admonition >}}

