OpenClaw 架构治理手记
本地的 Studio 能管 agent(智能体),Mission Control(任务中控)的网页端也能管 agent,将来要装的 Gateway(网关)打算把请求路由到 agent 上——三套都能管,三套都有道理。问题不在哪一套不好,问题在三套放在一起的时候,谁说了算开始变得不清楚。
这件事最近一直挂在我的待办列表里。每一步当时看都对——本地 Studio RC1(首个候选发布版)跑起来的时候该有自己的边界,Mission Control Web v2.0.1(Alpha,早期开发阶段)作为 agent 编排仪表盘也该有自己的 dashboard(仪表盘),未来的 Gateway 作为外部入口也该有自己的路由规则。但三件单独正确的事被同时塞进同一个工作系统,重叠就来了。
本月我没急着收口,先把「谁该回答什么问题」写清楚——这一步比加任何新功能都重要。下面是我现在的分工判断、几个具体冲突点,以及为什么我决定先写 spec(规约),不动代码。
每一套单独看都合理
先把三套各自的来源说一遍。不讲清楚每一套为什么存在,冲突的时候就没法判断该让谁让步。
本地 Studio RC1 是本地工作系统。它最初的存在理由很朴素——我需要一个不依赖外部接口就能完成晨检、任务流转、内容产出的底座。所有 agent 的契约(读哪里 / 写哪里 / 不能动哪里)都签在本地,所有任务的 audit(审计)都落在本地审计目录,所有内容产出的 evidence(证据)也都留在本地。Studio 的设计立场一直是「本地真相」——我不信任何不在本地能复现的状态。这条立场不是技术品味,是被现实逼出来的:外部接口随时会变,远端服务随时会挂,第三方记录随时会被改写,本地审计是唯一能在事后追回当时发生过什么的依据。
Studio 的设计还有一层含义:agent 的契约必须在本地能被读到。一个 agent 能读哪些目录、能写哪些目录、绝对不能动哪些目录,RC1 阶段就被定成了文件——不是数据库里的一行,也不是远端配置中心里的一项。看起来死板,但它换来了一个性质:本地任何一次工具调用要被允许,都得回到这份契约文件查一次。Studio 的 agent 控制平面建立在本地文件系统之上,离开本地它什么都决定不了。这是它的强项,也是它的边界。
Mission Control Web 是另一回事。它是 Alpha 阶段的 v2.0.1,作为 Agent 编排仪表盘存在。要解决的问题很直接——agent 不止一个、任务不止一条、外部模型不止一家的时候,光看本地日志已经看不过来了。MC Web 想做的是舰队视角:多 agent 同时跑、调度可视化、cost tracking(成本追踪)、安全审计 dashboard。它在网关适配层留了多框架的口子,状态写在 SQLite 里,pnpm start 起服务,整体偏前端工程师视角。这套东西的设计立场是「全局观察」——我得能一眼看到全部 agent 现在在干嘛、烧了多少钱、有没有越界。
MC Web 还有一条设计目标常被忽略——跨框架管 agent。本地的 agent 是一种实现,未来用 DeerFlow 跑一条研究流程是另一种实现,更远的将来接入别的编排框架又是一种实现。MC Web 不该绑死在一种实现上,网关适配层才留了多框架的口子。代价是对每一种具体 agent 的语义只能做到「最小公分母」:能看到 agent 在跑,能看到烧了多少 token,但要看内部更细的状态,还得回到 agent 自己的实现里。这条限制是 MC Web 这一类工具的天然属性,不是 v2.0.1 的缺陷。
Gateway 还没装。它现在的样子只是 MC Web v2.0.1 里那个标成「Gateway Optional」的口子——团队知道总有一天会有这么个东西,但没决定什么时候装。Gateway 的设计意图很明确:所有外部进来的请求由它统一路由、统一鉴权、统一限流,不让外部直接打到本地或者打到 dashboard 上。它的设计立场是「边界守门人」——外部世界进来的任何东西先过我这道闸。
Gateway 的存在还有一个理由:分层防御。本地 Studio 和 dashboard 都不该直接面对公网。本地系统专心做本地能做的事,dashboard 专心做展示,外部请求该有一个专职的层去做最脏最累的事:拒绝异常流量、限制频率、做最外层的鉴权。这件事不交给一个专门的层,迟早会污染 Studio 或 MC Web——它们会因为外部的脏请求而被迫加入大量本来不该有的防御代码。Gateway 装不装是选择,但只要还没装,Studio 和 MC Web 暴露的端口就在默认承担「外部入口」的职责——这是个隐性的麻烦。
三套各自的立场——本地真相、全局观察、边界守门——单独看都成立。麻烦的是这三件事在 agent 这个对象上交叉。一个 agent 既要有本地契约,也要在 dashboard 上被看到,将来还要接受外部进来的请求。三套控制平面就这样开始重叠。
放在一起开始抢什么
重叠不是抽象问题,它有具体形状。最近这两周,我至少在四个地方撞到了。
第一个冲突:一个本地任务到底归谁调。Studio 自己有任务调度——晨检时它会触发苏晚去整理一篇内容,触发霍锐去做安全巡检,触发沈知行去拉信息。这些都是本地链路,Studio 自己说了算。但 MC Web 的 dashboard 上也有一个「任务调度」面板,理论上从那里也能下发一条「让苏晚现在跑一篇」的指令。两个入口都能调一个 agent,就是两条调用路径,两份调度状态。两边状态不一致——一个说在跑,一个说没跑——agent 该听谁的?这个问题以前没遇到,因为 MC Web 没真接进来;接进来的那一刻它就来了。
这个冲突最阴的地方在「两边都没怎么用的时候」根本看不到。本地 Studio 跑得稳,MC Web 当看板用着没人下发任务,一切都和谐。一旦哪天我图方便从 MC Web 触发了一条任务,本地 Studio 的调度状态里没有这条记录,过几个小时本地周期任务又被触发跑了一遍——同一件事跑了两遍,留下两份审计记录,两边都觉得自己是对的。这种重复触发不是技术 bug,是控制平面没说清楚谁是入口的副作用。
第二个冲突:一个 agent 的运行状态、成本、审计该写在哪里。Studio 有自己的本地 audit——每一次 agent 写文件、每一次任务流转、每一次工具调用都有记录,留在本地审计目录。MC Web 有 dashboard——要展示「过去 24 小时这个 agent 跑了多少次任务、消耗了多少 token、有没有越界」。两份数据各写各的,就是两份真相;其中一份是镜像,得先选谁是源。我现在的本能反应是 Studio 的本地 audit 是源——但这只是本能,没写进 spec 就不算分工。
审计这件事在控制平面里特别敏感。真相源不是被「指定」出来的,是被「真的写入」出来的——哪一份数据先被写下来、哪一份后被同步过去、两边不一致时采信哪一份,都要事先讲清楚。我以前遇到过的最难追的 bug,几乎都来自审计不止一份且没有公认源。这个冲突点我格外谨慎——agent 的真相源必须在一开始就定,不能等出问题再回头协调。
第三个冲突:将来 Gateway 装上以后,外部请求该先打谁。一种走法是 Gateway → Studio → MC Web(本地优先,dashboard 是观察侧);另一种走法是 Gateway → MC Web → Studio(编排优先,本地是执行侧)。两种走法都能跑通,但路径完全不同——前者意味着 MC Web 看到的永远是 Studio 已经处理过的二手信息,后者意味着 Studio 接到的请求都已经被 MC Web 的策略筛过一遍。这事现在不定,等 Gateway 真装上去再定,就是边装边改边纠错,代价很大。
第四个冲突更隐蔽:cost tracking 该算哪一层的责任。Studio 自己也能记录每次工具调用的成本——它有 audit,加一列字段就能记。MC Web 也能记——它本来就是为编排和观察设计的,cost 是它的天然字段。这件事现在两边都没好好做,暂时没冲突;一旦两边都开始认真做,又是两份成本数据,又是「以哪一份为准」的问题。更麻烦的是成本最终会被汇总到「这个月烧了多少钱」这种粒度——两份数据都存在,汇总时一定会双计或漏计,要么虚高要么虚低,没有中间状态。
这四个冲突点放在一起看,其实是同一个形状——三套系统在「谁拥有 agent 这个对象的某一面」上没有写清楚边界。Owner(所有权)没定,调度路径没定,真相源没定,成本归属没定。每一项单独看都不是大事,加在一起就是控制平面的扯皮。
重叠不是 bug,是规模副作用
我一开始想把这件事归咎于「当初设计没规划好」。后来发现这个归因不对。Studio 当年只是想解决本地能不能跑,MC Web 当年只是想解决多 agent 看不过来,Gateway 当年只是预留一个外部入口的口子。三件事不在一个时间点被设计,也不在一个语境下被设计。
重叠是规模长出来的,不是设计错出来的。一个组件刚生下来的时候只对自己的那一小块负责;跑稳了、跑久了、跑到一定规模,自然会开始把手伸向相邻的责任。Studio 跑到 RC1 之后开始想「我能不能也提供一个简单的看板」——这就是它伸向 MC Web 的领域;MC Web 跑到 v2.0.1 之后开始想「我能不能直接调度本地 agent 不经过 Studio」——这就是它伸向 Studio 的领域。每一个组件最初都不是为了和别的组件竞争而生的,但活到一定规模就开始抢同一块责任。
我觉得这是普遍现象。任何一个长出来的工作系统,活过初期之后都会面对控制平面重叠的问题——不是有人做错了,是活下来的组件自然会扩张。重叠是个体存活的副作用。
扩张的方向也有规律。一个最初只解决「能跑」的组件,跑稳之后第二个本能是「让我自己也能看到我在跑什么」——开始长出一个简单的查询接口、一个简陋的状态面板。这个面板一旦存在,责任就侵入「观察」这件事;原本专门做观察的组件就开始觉得「我看到的怎么和它自己看到的不一样」。第三个本能是「让外部也能调我」——原本只服务本地的组件会开始想给外部留一个调用入口。这个入口一旦存在,原本专门做外部入口的组件就开始觉得「为什么外部不走我这里」。本能是好的,组件想活得更强壮的本能是好的,但每一次本能扩张都会把控制平面的边界推糊一点。
意识到这一点之后,我对收口的看法变了。我以前倾向于「重叠了就赶紧砍掉一边」,但这种砍法往往砍掉的是当下最弱的一边——不是长期最不该负责这件事的那一边。短期看是收口了,长期看是被错位的力量推回去——被砍掉的责任过几个月还会以另一种形式长回来。
现在我处理重叠的方式不是先动刀,是先定立场——这件事长期该归谁,归不到位的那一边短期可以保留,但要标记成「过渡」。过渡很关键——它承认现状不理想,但不假装现状是理想,也不强行立刻整改。它给真正该负责的那一边时间去补能力,同时给暂时承担过渡责任的那一边一个退出预期。这种处理比一刀砍痛苦得多——要写更多 spec、要做更多沟通、要忍受更长的不一致——但能避免「砍完之后责任又长回来」的循环。
我现在的分工判断
这一轮我没动手砍,先停下来写分工——把「谁该回答什么问题」写在 spec 里,而不是先改代码。
下面这套分工是我现在的判断,不是已经落地的状态:
- Agent 的 owner——Studio。Agent 的契约(读哪里 / 写哪里 / 禁止哪里)在本地,agent 的版本、能力、稳定性记录也在本地。MC Web 看到的是 Studio 暴露出来的 agent 元信息,不是 MC Web 自己定义的。
- 任务的真相源——Studio 的本地 audit。每一次任务执行的原始记录留在本地,MC Web dashboard 上看到的是同一份记录的展示,不是另一份独立数据。
- 编排和观察——MC Web。多 agent 舰队视角、任务调度可视化、安全审计 dashboard、跨 agent 的统计聚合,都归 MC Web。它不是 audit 的来源,是 audit 的视图。
- Cost tracking——MC Web。成本数据天然跨 agent、跨外部网关、跨模型供应商,视角应该在更高一层;Studio 不再自己单独算成本。
- 外部入口——Gateway。所有外部进来的请求由它统一收,做完鉴权、限流、路由之后,再决定打给 Studio 还是打给 MC Web。Studio 和 MC Web 都不再暴露外部端口。
- 调度入口——双入口,但 Studio 优先。本地周期性任务由 Studio 自己调;从 MC Web dashboard 触发的任务最终也要走 Studio 的调度器,不是 MC Web 直接调 agent。
这套分工里其实只有一句话——agent 的本地真相归 Studio,全局视图和外部边界归外面。四个冲突点都能从这一句话推出答案。花时间先写分工比直接动手值得:一句对的话能解决一堆细的问题。
分工写清楚不等于落地
话又说回来,写在 spec 里不等于已经能跑。我自己最清楚——这套分工真的落地,至少要半年。
第一件要做的是 API 边界。Studio 现在没有一个对外的 agent 元信息接口——agent 的契约是文件,不是接口。MC Web 想看到这些信息只能去读文件。要让 MC Web 真的「看到 Studio 暴露的 agent 元信息」,得先在 Studio 这边定义一组接口,把 agent 列表、agent 当前状态、agent 当前任务这些字段以稳定的契约暴露出来。这件事说起来一句话,做起来要梳整整一个版本——哪些字段稳定、哪些可变、版本怎么演进、向后兼容怎么办,都要单独想清楚。
API 边界还有一件事要想清楚——Studio 没起来的时候怎么办。MC Web 永远只通过 Studio 暴露的接口看 agent 信息,Studio 离线的时候 MC Web 就是个空壳;MC Web 保留一份本地缓存来应对 Studio 离线,缓存又要单独维护,缓存陈旧的时候 dashboard 上看到的就不再是真相。两种选择都有代价,但都比「没想清楚就先实现一个版本」要稳。我倾向于第一种——宁可空壳,不要假数据——但这一条还没真定,要等 MC Web 那边的人也表态。
第二件要做的是调度路径的梳理。Studio 自己有调度器,MC Web 也想做调度入口,两边没有共识的入口规范。理顺这一块需要把所有「能触发一个 agent 跑起来」的代码路径列出来,然后收口到一条主路径上。这种梳理是慢活——每一条路径都要单独验证「收口之后是不是还能跑」。
调度路径里最讨厌的是那些「侧门」——不是主流程的入口,但确实能触发 agent 跑起来的小路径。比如某个本地脚本里直接调用了 agent 的执行函数,跳过了 Studio 的调度器;比如某个测试用例里 mock 出来的触发路径在生产代码里也能跑通。这些侧门平时没人走,但只要存在,分工就不算清楚。梳理的过程就是把所有侧门列出来然后逐个堵掉——很枯燥,不能跳。
第三件要做的是 audit 同步机制。Studio 的本地 audit 是真相源,MC Web 的 dashboard 是镜像——这句话写得轻巧,但「镜像」是一个需要机制去维护的状态。本地 audit 写入之后多久同步到 MC Web,同步失败了怎么办,MC Web 那边的展示和本地 audit 不一致的时候以哪一份为准,这些都要单独定。我现在的偏向是 MC Web 永远只读 Studio 暴露的接口,不维护自己的写入路径——但这意味着 MC Web 在 Studio 不可达的时候 dashboard 是空的,又是另一个权衡。
audit 同步还有一层我以前没意识到的隐含问题——涉及隐私边界。本地 audit 里有些信息本来就不该被推到 dashboard 上展示——比如某些任务的中间产物、某些 agent 的内部状态、某些和外部账号相关的字段。同步机制不能简单做成「全推过去」,要有一层过滤。这层过滤本身又是一个 spec——哪些字段可推、哪些不可推、可推的是否需要脱敏,都要写清楚。这件事从 Gateway 那边看更容易做(外部入口本来就要做这种过滤),但 Gateway 还没装,这层责任暂时还在 audit 同步机制这边。
第四件,其实也是 Gateway 真装上去之前的预备工作,是把 Studio 和 MC Web 各自的对外暴露面收窄。现在它们俩各自暴露自己的端口,Gateway 装上去之后这些端口都要藏到 Gateway 后面。这件事的代价不是技术上的,是迁移上的——所有已经存在的、直接打 Studio 端口的调用方都要改路径。
收窄暴露面这件事不是改一个配置就完。所有外部脚本、所有外部集成、所有曾经偷偷直连过来的小工具都要重新走 Gateway——而 Gateway 还没装。所以这一件事最务实的做法是先把暴露面盘清楚:现在有哪些端口在外部可达、每一个端口的调用方有谁、这些调用方是否还在被使用。盘清楚之后才能谈收窄。这种盘点是「无聊但必要」的事——本身不产生新功能,但它是分工真正能落地的前提。
本月我只做了第一件的一部分——梳理了几个 API 边界的草稿,把 agent 元信息、agent 当前状态这两组字段先定下来。其它三件都还在排队。我不想骗自己说「分工定了就等于做完了」——分工定了只是第一步,落地是更长的事。
关于这件事我还在改
分工现在写在 spec 里,但还没有任何一行代码因为这套分工被改动过。MC Web 暂时还是按自己的节奏跑 v2.0.1,Studio 还是按 RC1 的边界跑,Gateway 还在「Optional」状态。这套分工要变成代码里的事实,至少要半年——前提是中间没有别的更紧急的事插队进来。
本月真正完成的没几件——agent 元信息 API 的字段草稿、agent 当前状态 API 的字段草稿,以及一份冲突点清单。冲突点清单本身没解决任何冲突,但它让我每次再撞到一个新冲突的时候知道这是不是已经被记过的、能不能用同一套分工答上。
MC 和 Studio 的 audit 同步机制还没动。调度路径还没收口。Gateway 装不装、什么时候装、怎么装,都还没有时间表。我现在不急着定——经验告诉我,这种「装外部入口」的事在分工没真正落实之前就强推,会把分工本身搅烂。
我对这件事的态度是:先把分工写清楚比先把功能加上去重要,先把边界收口比先把版本号往前推重要。Mission Control v2.0.1 是 Alpha,迭代速度还会很快;Studio RC1 是候选发布版,但本地链路也还在持续被改;Gateway 八字没一撇——三套都在变的时候,唯一不变的应该是分工。
没有「完美架构」这种东西,只有「职责清楚 + 边界写出来」的架构。完美架构是想出来的,职责清楚是改出来的;前者一画图就完,后者每个月都要回头校一次。
Mission Control、Studio、Gateway 这三套控制平面会长期共存,重叠不会被一次性消除——它会以分工是否清楚为准、被反复重新平衡。这件事我没有终点表,只有每月推一小步的节奏。
下一次再写这件事,多半是从「某一条 API 边界终于落地」或者「某一次以为收口了结果又长回去」开始讲。在那之前,我还在改。