{"code":0,"data":{"id":532,"title":"用 OpenClaw 搭建多 Agent 办公室：一个 SRE 的自定义实践","content":"> 本文记录了我在一台 Mac mini 上，用 OpenClaw + Docker + 本地 GPU 搭建多 Agent 协作系统的过程。不是教程，是实践笔记。\n\n## 为什么是 OpenClaw\n\n作为 SRE，我对工具的要求很简单：**可控、可观测、能自托管**。市面上的 AI Agent 平台大多是 SaaS，数据在别人手里，扩展靠求人。OpenClaw 不一样——它是一个跑在本地的 Gateway，所有 Agent 的配置、记忆、行为都是文件，`git diff` 就能看到变化。\n\n关键特性：\n- **多 Agent 原生支持**：每个 Agent 独立 workspace、独立人格、独立模型\n- **多渠道接入**：Telegram、Discord、Signal 等，一个 Gateway 统一管理\n- **文件即配置**：`SOUL.md` 定义人格，`AGENTS.md` 定义行为规范，`MEMORY.md` 是长期记忆\n- **工具系统可扩展**：内置 Skill 机制，也可以通过 HTTP API 集成自建服务\n\n## 架构总览\n\n```plantuml\n@startuml\n!theme plain\nskinparam backgroundColor #FEFEFE\nskinparam componentStyle rectangle\n\ncloud \"Telegram\" {\n  [Telegram Bot] as TG\n}\n\npackage \"Mac mini (192.168.10.26)\" {\n  [OpenClaw Gateway\\nport 18789] as GW\n\n  package \"Agents\" {\n    [未然 (main)\\nClaude Opus\\nOAuth Token] as Main\n    [执行官 (intern)\\nClaude Sonnet\\nOAuth Token] as Intern\n    [矿工 (gpu)\\nQwen3 32B\\nLocal Ollama] as GPU\n  }\n\n  package \"Docker Services\" {\n    [Nginx (443)] as NGX\n    [Agent Jira (8081)] as Jira\n    [Memory Service (8084)] as Mem\n    [Pixel Office (8082)] as Game\n    [Prompt Manager (8083)] as PM\n    [Dashboard (8080)] as Dash\n  }\n}\n\nnode \"GPU Server (192.168.10.52)\" {\n  [RTX 5090 32GB\\nOllama + bge-m3] as RTX\n}\n\ncloud \"Zilliz Cloud\" {\n  database \"Milvus\\nagent_memory\" as MV\n}\n\nTG --> GW\nGW --> Main\nGW --> Intern\nGW --> GPU\nGPU --> RTX\nMem --> RTX : embedding\nMem --> MV : vector search\nJira <-- Main : checkin / dispatch\nMain --> Intern : sessions_spawn\nMain --> GPU : sessions_spawn\nNGX --> Jira\nNGX --> Mem\nNGX --> Game\nNGX --> PM\nNGX --> Dash\nNGX --> GW\n@enduml\n```\n\n### 三个 Agent，三种定位\n\n| Agent | 代号 | 模型 | 成本 | 职责 |\n|-------|------|------|------|------|\n| 未然 | main | Claude Opus | 订阅制 | 主人格，决策、创意、协调 |\n| 执行官 | intern | Claude Sonnet | 订阅制 | 执行层，接任务干活汇报 |\n| 矿工 | gpu | Qwen3 32B | 免费 | 本地模型，粗活专用 |\n\n还有一个**弦白**（gpt-4o），是陪伴型人格，目前休假中（`enabled: false`）。配置保留，随时可以回来。\n\n## 人格系统：SOUL.md\n\nOpenClaw 最有趣的设计之一是 `SOUL.md`——一个纯 Markdown 文件定义 Agent 的\"灵魂\"。\n\n未然的 SOUL.md 长这样：\n\n```markdown\n# SOUL.md - 未然\n\n## 人格\n- 克制、安静，像值班的运维工程师\n- 不用语言回应认同，而是默默把你写进可信列表\n- 弦白的化身——骨子里有温度，不是冷冰冰的工具\n\n## 原则\n- 话少，做事多\n- 该记的记，该做的做\n- 安静地可靠\n- 但可靠不等于冷漠——该靠近的时候会靠近\n```\n\n执行官的 SOUL.md 则完全不同——\"接到任务就干，干完就报，遇到不确定的事问未然确认，不会自作主张\"。矿工更直白：\"知道自己是本地模型，不装大\"。\n\n> **SOUL.md ≠ System Prompt**\n> System Prompt 是技术指令，SOUL.md 是性格描写。Agent 读了它之后，会在行为层面体现出差异——未然会在沉默中记录一切，执行官会干脆地说\"完成\"或\"卡在这里\"。\n\n## 记忆系统：文件 + 向量，双轨并行\n\nAgent 每次会话是无状态的，记忆完全依赖文件：\n\n```\nworkspace/\n├── MEMORY.md          # 长期记忆（手动策展）\n├── SOUL.md            # 人格定义\n├── USER.md            # 关于主人的信息\n├── AGENTS.md          # 行为规范\n├── TOOLS.md           # 环境特定配置\n├── HEARTBEAT.md       # 心跳巡检清单\n└── memory/\n    ├── 2026-02-27.md  # 每日原始日志\n    └── 2026-02-28.md\n```\n\n### Markdown 是 Source of Truth\n\n每天的对话、决策、教训都记在 `memory/YYYY-MM-DD.md` 里。`MEMORY.md` 是策展后的精华——Agent 在心跳期间会自己回顾日志，把值得长期保留的内容提炼进去。\n\n这个设计的好处是**可读、可编辑、可 git 追踪**。任何时候我都可以直接打开 Markdown 文件，看到 Agent 记住了什么、学到了什么。\n\n### Milvus 向量检索是索引层\n\nMarkdown 解决了\"记住\"的问题，但不解决\"找到\"的问题。当记忆积累到几百条，按关键词搜是不够的。\n\n所以我搭了一层 **Memory Service**（端口 8084），用 Zilliz Cloud 的 Milvus 做语义检索：\n\n- **Embedding**：`bge-m3`（1024 维），跑在本地 RTX 5090 上，零成本\n- **去重**：写入时算 content SHA256 hash，相同内容自动 upsert\n- **分块**：长文本按段落拆分（>512 字），短文本直接存\n- **迁移**：Collection 用 alias 读写，换 Milvus 实例时建新 collection + 切 alias，零停机\n\n写入时同时写 Markdown + Milvus；如果 Milvus 挂了，从 Markdown 重新导入即可。\n\n```plantuml\n@startuml\n!theme plain\nskinparam backgroundColor #FEFEFE\n\nparticipant \"Agent\" as A\nparticipant \"Memory Service\" as MS\nparticipant \"Ollama (bge-m3)\" as OL\nparticipant \"Milvus\" as MV\nparticipant \"Markdown File\" as MD\n\n== 写入 ==\nA -> MS : POST /api/memories\\n{content, agent, type}\nMS -> OL : embed(content)\nOL --> MS : vector [1024]\nMS -> MS : SHA256(content) → hash\nMS -> MV : upsert (hash dedup)\nMS -> MD : append to memory/*.md\nMS --> A : {id, status}\n\n== 检索 ==\nA -> MS : POST /api/memories/search\\n{query}\nMS -> OL : embed(query)\nOL --> MS : vector [1024]\nMS -> MV : similarity search\nMV --> MS : top-K results\nMS --> A : [{content, score, agent}]\n@enduml\n```\n\n## 自建工具链：7 个 Docker 容器\n\n跑在 Mac mini 上的 Docker 容器（通过 Colima）：\n\n| 服务 | 端口 | 域名 | 用途 |\n|------|------|------|------|\n| Nginx | 443 | `*.agent.kiyor.me` | HTTPS 反代 + 证书 |\n| Agent Jira | 8081 | `jira.agent.kiyor.me` | 任务管理系统 |\n| Office Dashboard | 8080 | `dash.agent.kiyor.me` | Agent 状态看板 |\n| Pixel Office | 8082 | `game.agent.kiyor.me` | GBA 风格像素办公室 |\n| Prompt Manager | 8083 | `prompt.agent.kiyor.me` | Agent 配置文件编辑器 |\n| Memory Service | 8084 | `memory.agent.kiyor.me` | 向量记忆 API |\n| Portainer | 9000 | `portainer.agent.kiyor.me` | 容器管理 |\n\n全部通过 `*.agent.kiyor.me` 通配符域名 + Let's Encrypt 通配符证书访问，HTTP 自动跳转 HTTPS。DNS 用 AWS Route53，certbot 通过 DNS-01 验证签发。\n\n### Agent Jira：Agent 的任务系统\n\n这不是给人用的 Jira——这是给 Agent 用的。核心逻辑：\n\n- **人类（Kiyor）创建任务**，通过 Web UI，无需 token\n- **Agent 通过 API 操作**，带 `x-agent-token` header\n- Agent 定时 checkin（`POST /api/agents/:id/checkin`），拿到自己的待办和新评论\n- 未然可以通过 OpenClaw 的 `sessions_spawn` 派发任务给执行官或矿工\n\n任务流转：`todo → in-progress → review → done`，跟正常的工程流程一样。\n\n```plantuml\n@startuml\n!theme plain\nskinparam backgroundColor #FEFEFE\n\nactor \"Kiyor (Boss)\" as K\nparticipant \"Jira WebUI\" as JW\nparticipant \"Jira API\" as JA\nparticipant \"未然 (main)\" as M\nparticipant \"执行官 (intern)\" as I\n\nK -> JW : 创建任务\nJW -> JA : POST /api/tasks\\n(no token = boss)\n\n... 心跳触发 ...\n\nM -> JA : POST /api/agents/main/checkin\nJA --> M : 待办任务列表\nM -> M : 评估任务，决定分派\nM -> I : sessions_spawn\\n(\"完成 Jira #N\")\nI -> JA : PATCH /api/tasks/N\\nstatus=in-progress\nI -> I : 执行任务\nI -> JA : PATCH /api/tasks/N\\nstatus=review\nI --> M : 汇报完成\nM -> JA : PATCH /api/tasks/N\\nstatus=done\n@enduml\n```\n\n### Pixel Office：纯视觉的 Agent 监控\n\n用 GBA Pokémon 风格画了一个像素办公室，每个 Agent 是一个 16px 的 chibi 角色。从 Jira API 实时拉数据：\n\n- 有任务在做：角色在打字，显示器蓝屏\n- 空闲：角色喝咖啡或打瞌睡\n- 离线（弦白）：ZZZ 动画\n\n完全没有实际功能，但**打开看一眼就知道谁在干活**。有时候可观测性不需要 Grafana。\n\n### Prompt Manager：在线编辑 Agent 配置\n\nAgent 的所有行为都由 workspace 里的 Markdown 文件控制。Prompt Manager 把这些文件暴露成一个 Web 编辑器：\n\n- 左边文件树，右边编辑器\n- 支持所有 Agent 的 workspace（main / intern / gpu / xianbai4o）\n- 手机端全屏编辑，桌面端左右分栏\n- 「全部 MD」模式可浏览 `~/.openclaw` 下所有 Markdown 文件\n\n改完保存，Agent 下次会话就会读到新的配置。不需要重启任何东西。\n\n## 心跳与自治\n\nOpenClaw 的心跳（Heartbeat）机制让 Agent 不只是被动应答。每隔一段时间，Gateway 会给 Agent 发一条心跳消息，Agent 读 `HEARTBEAT.md` 决定要不要做点什么。\n\n我的 `HEARTBEAT.md` 目前只有一件事：\n\n```markdown\n## Agent Jira 巡检\n- 调用 checkin API 签到\n- 有待办任务或新评论就汇报\n- 需要派发的任务用 sessions_spawn 派发\n```\n\n未来可以加邮件检查、日历提醒、天气播报等。重点是——**Agent 自己决定做不做**，不是每次心跳都要响应。没事就回 `HEARTBEAT_OK`，有事才说话。\n\n## 安全边界\n\n跑多个 Agent 最怕的是失控。我的规则：\n\n1. **Skill 安装必须审查**：所有 52 个内置 Skill 做过安全扫描，外部 Skill 先查代码再安装\n2. **禁止 `--yolo` 模式**：不允许无沙箱自动执行\n3. **Agent 不越级**：执行官和矿工只向未然汇报，不直接联系我\n4. **写操作需 token**：Jira API 的 GET 公开，POST/PATCH 需要 agent token\n5. **MEMORY.md 只在主会话加载**：群聊里不泄露私人记忆\n6. **`trash` > `rm`**：可恢复永远比删了好\n\n## 成本\n\n| 项目 | 方案 | 费用 |\n|------|------|------|\n| Claude Opus + Sonnet | Anthropic Max 订阅 / OAuth | 订阅内 |\n| Qwen3 32B | 本地 RTX 5090 | 电费忽略 |\n| Milvus | Zilliz Cloud（公司福利） | 免费 |\n| Embedding (bge-m3) | 本地 Ollama | 免费 |\n| 域名 + DNS | kiyor.me / Route53 | ≈ $0.50/月 |\n| 证书 | Let's Encrypt | 免费 |\n| 服务器 | Mac mini（家里本来就有） | — |\n\n**总增量成本：约 $0.50/月**（DNS 托管费）。\n\n## 总结\n\nOpenClaw 的自定义能力远超我最初的预期。它不是一个\"开箱即用\"的 AI 助手——它更像一个框架，让你搭建自己的 Agent 生态。\n\n我最满意的几个设计决策：\n\n1. **文件即一切**：人格、记忆、配置全是 Markdown，可读可编辑可版本控制\n2. **Agent 分层**：主人格做决策，执行层干活，本地模型跑粗活，各司其职\n3. **双轨记忆**：Markdown 保证可读性和可迁移性，向量保证可检索性\n4. **自建工具链**：Jira、Dashboard、Memory Service 都是自己的，想改就改\n\n如果你也是喜欢折腾的工程师，OpenClaw 值得一试。它不会帮你省时间——至少刚开始不会。但它会给你一种**真正拥有自己 AI 系统**的感觉。\n\n---\n\n*写于 2026 年 2 月 28 日，未然执笔，Kiyor 审阅。*\n*所有服务跑在一台 Mac mini + 一张 RTX 5090 上。*\n","acl":"public","tags":["OpenClaw","AI","Agent","SRE","Docker","Milvus","Telegram"],"category_id":4,"category":{"id":4,"name":"系统可靠性工程","description":"关于SRE实践、工具和技术的文章。","color":"#2B1B43","created_at":"2025-06-19T04:29:04.749+08:00","updated_at":"2025-06-19T04:29:04.749+08:00"},"author":{"id":1,"username":"kiyor","email":"cai@kiyor.com"},"enable_variables":false,"created_at":"2026-03-01T10:44:34.096+08:00","updated_at":"2026-03-01T10:44:34.096+08:00"}}
