# 为什么 Agent 系统首先需要一个控制平面

> 从模型桥接器到运行时治理 —— 构建一个 7 Provider、610 测试、36 个版本的 Agent Runtime 的经验

---

## 没人讨论的问题

所有人都在造 Agent 系统。很少有人在治理它们。

典型的 Agent 架构在白板上画起来很清晰：用户 → 大模型 → 工具 → 响应。但一旦上了生产环境，你很快会发现——真正的难题不在于让大模型更聪明，而在于让整个系统**可控**。

当你部署一个连接外部大模型、代替用户执行工具的 Agent 时，以下问题会立刻出现：

- **Provider A 挂了。** 系统是直接报错？无限重试？还是自动切到 Provider B？多快能切？
- **大模型幻觉了一个工具调用**，参数名写错了。工具会崩溃吗？用户会看到报错吗？
- **对话历史增长到 300KB。** 请求会超时吗？会吃光整个上下文窗口吗？
- **定时任务 6 小时没跑了。** 你能发现吗？有人会被告警吗？
- **两个记忆层返回了矛盾的信息。** 大模型该信谁？

这些都不是能力问题。它们是**治理问题**。解决它们需要一种不同的架构：控制平面。

## 什么是 Agent 控制平面？

借用网络工程和 Kubernetes 的概念，控制平面是**管理系统如何运行**的一层，与实际干活的数据平面分离。

```
┌─────────────────────────────────────────────────┐
│                 控制平面                          │
│  策略  │  路由  │  可观测性  │  故障恢复          │
└──────────────────────┬──────────────────────────┘
                       │ 治理
┌──────────────────────▼──────────────────────────┐
│                 能力平面                          │
│  大模型调用  │  工具执行  │  智能路由             │
└──────────────────────┬──────────────────────────┘
                       │ 记忆
┌──────────────────────▼──────────────────────────┐
│                 记忆平面                          │
│  KB 搜索  │  多媒体  │  偏好  │  状态             │
└─────────────────────────────────────────────────┘
```

对于 Agent 系统，控制平面负责的事情：

| 治理关注点 | 做什么 | 没有它会怎样 |
|-----------|--------|------------|
| **Provider 路由** | 为每个请求选择正确的模型 | 硬编码到一个 Provider，没有降级 |
| **工具治理** | 工具白名单、修复错误参数、强制限制 | 大模型随意调用工具，参数出错 |
| **请求整形** | 截断超大消息、管理上下文预算 | 上下文溢出、超时、OOM |
| **断路器** | 检测故障、路由到备用、自动恢复 | 级联故障、请求卡死 |
| **可观测性** | 追踪延迟/成功率/降级率，历史趋势 | 生产环境盲飞 |
| **审计** | 链式哈希记录状态变更，防篡改 | 出了事没法查，没法追责 |
| **记忆治理** | 跨层去重、置信度加权、冲突消解 | 大模型收到矛盾的上下文 |

## 核心洞察：治理必须先行

> "能力越强，系统越难控制——治理必须走在能力前面，而不是跟在后面。"

这违反直觉。造 Agent 的时候，本能是先堆能力：加更多工具、接更多模型、支持更多模态。治理？感觉是以后再说的事。

但实践中，每加一个没有治理的能力，就制造了**不受控的爆炸半径**：

- 加了新 Provider 但没有降级路由？一次 DNS 变更就能搞垮你的系统。
- 让大模型随意调工具？一个幻觉参数就能损坏你的数据。
- 扩大了上下文窗口但没有截断策略？一次长对话消耗 10 倍 token 预算。
- 加了记忆层但没有去重？大模型从三个来源看到同一篇论文三次。

我们在 36 个版本中总结的模式：**先建控制平面，再在其中添加能力。** 不要反过来。

## 实践：三个平面的架构

### 控制平面——管控者

控制平面是最厚的一层，触及每一个请求。

**断路器**——跨 7 个大模型 Provider 的零延迟故障转移：

```python
class CircuitBreaker:
    def is_open(self):
        if self.consecutive_failures < threshold:
            return False              # 闭合：走主路径
        if time.time() - self.open_since >= reset_seconds:
            return False              # 半开：允许一次探测
        return True                   # 打开：跳过主路径，直接降级
```

- **Provider 兼容层**：7 个 Provider（Qwen3、GPT-4o、Gemini、Claude、Kimi、MiniMax、GLM），标准化认证、能力声明、兼容性矩阵
- **工具白名单**：14 个标准工具 + 2 个自定义工具（search_kb、data_clean），Schema 简化，7 类畸形参数自动修复
- **请求整形**：基于上下文使用率的动态截断（>85% → 激进裁剪至 50KB，>70% → 适度裁剪至 100KB）
- **SLO 仪表盘**：5 项指标 + 历史快照 + sparkline 趋势 + 阈值告警
- **安全边界**：所有服务绑定 localhost，API Key 仅通过环境变量传递，自动泄漏扫描，安全评分 93/100

### 能力平面——执行者

- 多 Provider 路由（Qwen3-235B 主力 → Gemini 降级，0ms 切换）
- 多模态：文本 → Qwen3，图片 → Qwen2.5-VL（从消息内容自动检测）
- 自定义工具注入：data_clean 和 search_kb 被 Proxy 拦截，在本地执行
- 智能路由：简单查询 → 快速模型，复杂查询 → 完整模型

### 记忆平面——记忆者

这是 v2 架构增加最大价值的地方。5 个散落的脚本变成了统一的记忆系统：

```python
# 一次查询搜索所有记忆层
results = memory_plane.query("Qwen3 性能")
# → KB 语义结果 + 多媒体匹配 + 相关偏好 + 活跃优先级
# → 跨层去重移除重复
# → 置信度加权排序：KB(1.0) > 多媒体(0.85) > 状态(0.7) > 偏好(0.6)
# → 冲突消解标记各层之间的矛盾
```

- **4 层统一**：KB 语义搜索（本地 embedding）、多媒体记忆（Gemini embedding）、用户偏好（自动学习）、运维状态
- **跨层去重**：不同层返回同一文件名或相似文本 → 合并，保留最高分
- **置信度加权**：按层权重 + 新鲜度衰减（KB 结果超过 72h 降权）
- **冲突消解**：偏好与活跃优先级矛盾时 → 标注冲突、降分、让大模型判断
- **优雅降级**：任何一层不可用不影响其他层

## 证据：7 个故障注入实验

我们构建了一个可靠性评测工具（Reliability Bench），模拟 7 种生产故障场景。全部基于 mock，3 秒内跑完，已集成到 CI：

| # | 场景 | 注入 | 控制平面响应 | 检查 |
|---|------|------|------------|------|
| 1 | Provider 宕机 | 连续 3 次失败 | 断路器打开 → 降级 → 自动恢复 | 10/10 |
| 2 | 后端超时 | 服务器永远不响应 | 1 秒超时，无线程泄漏 | 2/2 |
| 3 | 畸形参数 | 错误参数名/多余字段/无效 JSON | 自动修复：7 类别名映射 + 剥离 | 7/7 |
| 4 | 超大请求 | 407KB 对话历史 | 截断至 197KB，保留系统消息 + 最近对话 | 6/6 |
| 5 | KB 未命中 | 不存在的主题 | 优雅的空结果响应 | 9/9 |
| 6 | Cron 漂移 | 心跳过期 2 小时 | 检测到过期，34 条注册表全部验证通过 | 5/5 |
| 7 | 状态损坏 | 无效/截断/空 JSON | 检测到损坏，原子写入防止腐蚀 | 8/8 |

**结果：7/7 通过，47/47 检查。** 没有控制平面的话，场景 1-4 每一个都会导致用户可见的故障。有了控制平面，这些故障被透明地处理了。

### 生产环境 SLO 实测

来自真实生产数据：

| SLO 指标 | 目标 | 实际 | 判定 |
|---------|------|------|------|
| 延迟 p95 | ≤ 30s | 459ms | PASS |
| 超时率 | ≤ 3% | 0% | PASS |
| 工具调用成功率 | ≥ 95% | 100% | PASS |
| 降级率 | ≤ 5% | 1% | PASS |
| 自动恢复率 | ≥ 90% | 100% | PASS |

### 故障恢复时间

| 故障模式 | 检测 | 恢复 | 用户影响 |
|---------|------|------|---------|
| 主力大模型挂了 | 立即 | 0ms 切换，300s 自愈 | 使用备用模型 |
| 后端超时 | 可配置（1-300s） | 立即返回错误 | 用户重试 |
| 工具参数畸形 | 立即 | 0ms 自动修复 | 无（透明处理） |
| 请求过大 | 立即 | 0ms 截断 | 旧上下文被丢弃 |
| 状态文件损坏 | 下次读取时 | 原子写入防止 | 如果写入是原子的则无影响 |

## 36 个版本的教训

### 1. 610 个测试 ≠ 系统正常

我们有 393 个测试通过的时候，PA（个人助手）告诉用户"我没有任何项目"。测试验证了组件；故障在**组件之间的接缝处**——系统提示是空的，共享状态没有被消费。教训：**测试系统，而不仅仅是部件。**

### 2. 每一层安全机制都是潜在的故障源

一次 crontab 事故（`echo | crontab -` 清空了所有定时任务）后，我们加了三层保护。然后我们不得不调试保护层本身。教训：**加安全之前先问"谁已经在管这件事了？"**

### 3. 没有治理的记忆就是噪声

我们有 5 个记忆组件产出结果。但没有去重的话，大模型从三个来源看到同一篇论文三次。没有置信度加权的话，一条过期的偏好排在新鲜的语义匹配前面。没有冲突消解的话，矛盾的信号让模型困惑。教训：**记忆也是一个治理问题。**

### 4. 原子写入不可商量

所有状态文件都用 tmp-then-rename 模式。一次写入中途的崩溃就会损坏状态。用原子写入，你要么拿到旧版本，要么拿到新版本，永远不会拿到半成品。

### 5. 重要的版本号是 /health 里的那个

我们把语义化版本号（`0.36.0`）写入了每个 `/health` 端点。调试生产问题时，第一个问题永远是"实际在跑的是哪个版本？"——而不是你以为在跑的版本。

## 核心论点

Agent 系统的能力在飞速增长。模型更聪明，工具更强大，上下文窗口更大，记忆系统更丰富。但没有控制平面：

- **故障级联**，因为没有断路器
- **成本失控**，因为没有请求整形
- **记忆自相矛盾**，因为没有跨层治理
- **无法调试**，因为没有可观测性
- **恢复靠人工**，因为没有自动愈合

Agent 生态在构建越来越强大的能力平面。缺失的——也是我们花了 36 个版本构建的——是让这些能力达到生产级的治理层。

Agent 控制平面不是锦上添花。它是 demo 和系统的分界线。

> 先建控制平面，再在其中添加能力。不要反过来。

---

*本文基于 [openclaw-model-bridge](https://github.com/bisdom-cell/openclaw-model-bridge)（v0.36.0），一个开源的 Agent 运行时控制平面。7 个大模型 Provider，610 个测试覆盖 23 个测试套件，7 个故障注入场景，为 WhatsApp AI 助手提供 12 个月的生产服务。*
