最近在调研多智能体协作和 AI 编程工作流时,注意到一个挺有意思的项目:Murmell。它把自己定义为 "Collaborative cloud canvas for coding agents",也就是“面向编码代理的协作式云端画布”。这个定位很有意思,它不像传统 IDE 那样强调本地编辑,也不像聊天窗口那样只有对话流,而是把人和 AI 编码代理的工作空间抽象成一块共享画布,所有任务、变更、审查意见都在上面流动。
这篇文章会从一个相对系统的角度来拆解这类工具:它到底解决什么问题、核心能力有哪些、比较通用化的架构和实现思路是什么,以及如果要接入 Claude Code、Codex 这类编码代理,有哪些值得注意的集成方式和坑点。
1. 背景与核心概念
1.1 什么是 Collaborative Cloud Canvas
先解决概念问题。Collaborative Cloud Canvas,翻译过来是“协作式云端画布”。它本质上是一个运行在云端的、支持多端同步的共享可视化空间,这个空间里的所有元素——节点、连线、卡片、评论、状态标记——都会实时同步给所有参与者。
和传统的“白板类”协作工具不同,Murmell 这类画布的核心参与对象不只是人,还包括编码代理(Coding Agent)。
也就是说,它打破了“只能人画给人看”的限制,让 AI 代理也能在这个画布上创建节点、推送进度、请求审批,甚至留下中间决策的理由。
1.2 Coding Agents 是什么
Coding Agents 是指能够自主完成代码修改、文件读写、命令执行、测试运行等任务的 AI 智能体。和普通的代码补全工具不同,Agent 拥有“规划 -> 执行 -> 观察结果 -> 再规划”的循环能力。
比如:
- Claude Code:在终端中以会话方式执行编码任务。
- OpenAI Codex:云端沙箱里的编码代理,可以操作仓库、运行命令。
- Copilot Workspace:以任务为中心,生成计划并提交 PR。
这类工具的问题是:人和 Agent 交互的界面很割裂。Agent 的输出通常是日志流,人看到的是一串文字;Agent 做了什么、改了什么文件、为什么要这么改,很难一眼看清。Murmell 想解决的,正是这个问题。
1.3 它解决什么问题
用一个开发场景来理解:
假设你让一个编码 Agent 帮你实现“用户登录接口”。Agent 会做这些事情:读取代码、创建文件、修改路由、写测试、跑测试。如果所有动作都只出现在终端里,作为人类开发者,你很难在过程中及时判断方向对不对。
而在 Murmell 这类画布中,Agent 的每个关键动作都会变成一个可视节点:
- “读取了 src/auth/login.ts”
- “创建了 interfaces/login-payload.ts”
- “修改了路由配置”
- “测试通过,等待审批”
每个节点旁边可以挂上代码片段预览、执行日志、审批按钮。人工开发者只需要像看一张项目管理图一样,就能理解 Agent 的完整动作链。
所以,Murmell 解决的问题可以总结为三点:
- 让 AI 编码过程可视化。
- 让人类可以低成本地介入、审批和纠偏。
- 让多个 Agent 或人与 Agent 的协作有一个统一的共享空间。
2. 核心能力拆解
作为一类产品,面向编码代理的协作画布,通常会包含下面这些核心能力模块。了解这些模块,有助于理解 Murmell 的设计思路,也有助于后续做二次开发或者自建类似系统。
2.1 实时画布渲染
画布不只是“一块白板”,它需要支持多种节点类型、分组、连线,甚至支持缩放平移(Pan & Zoom)。
节点通常包括:
- 任务节点:描述一个待办目标。
- 文件节点:展示某个文件或某个代码片段。
- 审批节点:人工审查后才能继续。
- 状态节点:标记当前 Agent 的执行状态。
- 评论节点:人类或 Agent 留下的备注。
2.2 Agent 状态可视化
一个长期运行的编码任务,可能有多个阶段:
规划中 -> 读取文件 -> 生成代码 -> 运行测试 -> 等待评审 -> 完成画布需要将这些状态以可视化方式呈现,同时把关键动作序列记录下来。这种“动作流”回放能力,对调试 Agent 的行为、定位错误决策点非常有价值。
2.3 人工介入与指令通道
人类开发者需要能在画布上直接给 Agent 下达指令:
- 暂停当前任务。
- 修改某个文件节点的内容。
- 驳回当前的实现方案。
- 追加一条新的约束说明。
这就是“Human-in-the-loop”机制。画布不只被动展示,还要能反向控制 Agent。
2.4 多会话与历史回放
同一个项目可能同时跑多个 Agent 会话。每个会话在画布上表现为一组独立的“图层”或者“泳道”。同时,每次操作都需要有历史记录,支持回放和审计。
2.5 权限与协作边界
当画布上了云端,权限就变得很重要。谁可以看?谁可以编辑?谁能审批?这些都需要通过角色来区分。比如:
| 角色 | 查看 | 编辑节点 | 审批 | 控制 Agent |
|---|---|---|---|---|
| 开发者 | ✅ | ✅ | ✅ | ✅ |
| 技术负责人 | ✅ | ✅ | ✅ | ❌ |
| 访客 | ✅ | ❌ | ❌ | ❌ |
| Agent | ✅ | ✅ | ❌ | ❌ |
3. 架构设计原理
如果你想真正跑通一个类似 Murmell 的最小系统,核心不是画布 UI,而是“如何把 Agent 的事件流同步到画布上”。
3.1 事件驱动架构
画布上的每一个变化,本质上都是一条事件。常见的事件类型:
{ "type": "node.create", "payload": { "id": "node_123", "kind": "task", "title": "实现登录接口", "position": { "x": 120, "y": 200 } } }再比如 Agent 推送一条状态变更:
{ "type": "agent.status.update", "payload": { "agentId": "agent_001", "status": "running", "currentAction": "正在修改 src/auth/login.ts" } }这种“事件 + 画布对象”的做法,可以让前端只负责渲染,代理端只负责产生事件,双方解耦。
3.2 状态同步模型
画布状态同步有几种常见方案,各有优劣:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 全量快照轮询 | 实现简单 | 流量大、延迟高 | 低并发小团队 |
| WebSocket 增量事件 | 实时性好 | 需要处理乱序与重连 | 主流实时协作场景 |
| CRDT(无冲突复制数据类型) | 离线合并且无冲突 | 实现复杂、学习成本高 | 多人同时编辑同一节点 |
| Operation Transform | 经典协作算法 | 需要中心服务器 | Google Docs 类产品 |
对 Murmell 这类场景,实际应用中更推荐“WebSocket 增量事件 + 服务端存快照”的方案。画布上很多节点是 Agent 自动产生的,人挤人的高频冲突场景并不多,CRDT 反而会让系统复杂度上升。
3.3 Agent 适配层
这是理解这类工具最关键的模块。
编码 Agent 通常都有自己的工具调用机制,比如:
- Tool Use协议
- Function Calling
- MCP(Model Context Protocol)
画布系统需要把 Agent 的工具调用转换成画布事件。
举个例子,如果 Agent 调用了read_file工具:
Agent 调用 read_file("src/auth/login.ts")适配层可以把它转换成:
{ "type": "node.create", "payload": { "id": "file_node_001", "kind": "file", "title": "读取文件 src/auth/login.ts", "content": "export async function login() {}", "parentId": "task_node_001" } }这样,Agent 执行过程中访问过的每一个关键文件,都会在画布上形成一条可视链路。
3.4 认证与审计
因为云端画布会承载项目源代码片段和决策过程,认证和审计是必须考虑的安全边界。
推荐的安全基线:
- 使用 OIDC 或云厂商统一的身份提供商。
- Agent 接入使用独立的 API Key,最小权限范围。
- 所有 Agent 操作写入不可篡改的审计日志。
- 画布中的代码内容加密存储。
- 对敏感仓库禁止将完整代码内容同步到画布,只允许同步文件路径或摘要。
4. 最小原型实现方案
上面聊了很多概念,但空谈没有意义。这一节我提供一个“最小可运行的云端画布 + Agent 事件源”原型实现思路。
这里要说明一下:下面的实现不是 Murmell 的源码,而是为了帮助你理解同类产品的实现思路,用 Node.js + WebSocket 写一个能跑的最小示例。如果你只是使用 Murmell 产品,可以跳过这一节;如果你打算研究它的架构,或者自建类似工具,这部分就非常关键。
4.1 创建项目结构
murmell-demo/ ├── package.json ├── server.js ├── agent-worker.js └── public/ └── index.html4.2 初始化项目
mkdir murmell-demo cd murmell-demo npm init -y npm install express ws uuid4.3 WebSocket 服务端实现
服务端负责两件事:维护画布状态,把 Agent 产生的事件广播给所有前端画布。
// 文件:server.js const express = require('express'); const http = require('http'); const { WebSocketServer } = require('ws'); const { v4: uuidv4 } = require('uuid'); const app = express(); const server = http.createServer(app); const wss = new WebSocketServer({ server }); app.use(express.static('public')); // 保存画布中的节点对象 const canvasState = new Map(); function broadcast(message) { const data = JSON.stringify(message); wss.clients.forEach(client => { if (client.readyState === client.OPEN) { client.send(data); } }); } wss.on('connection', (ws) => { console.log('画布客户端已连接'); ws.send(JSON.stringify({ type: 'canvas.snapshot', payload: Array.from(canvasState.values()), })); ws.on('message', (raw) => { try { const msg = JSON.parse(raw.toString()); if (msg.type === 'node.create') { const node = { id: msg.payload.id || uuidv4(), kind: msg.payload.kind || 'task', title: msg.payload.title || '', content: msg.payload.content || '', position: msg.payload.position || { x: 0, y: 0 }, createdAt: Date.now(), }; canvasState.set(node.id, node); broadcast({ type: 'node.create', payload: node }); } } catch (err) { console.error('消息解析失败:', err.message); } }); }); server.listen(3000, () => { console.log('murmell-demo 运行在 http://localhost:3000'); });4.4 模拟编码代理的推送脚本
Agent 并不需要知道 WebSocket 细节,它只需要向本地端口发送事件。我们可以用一个脚本模拟一个 Agent 先后读取文件、生成代码、修改状态的动作。
// 文件:agent-worker.js const WebSocket = require('ws'); const ws = new WebSocket('ws://localhost:3000'); const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); ws.on('open', async () => { console.log('Agent 已连接到画布服务'); await sleep(500); ws.send(JSON.stringify({ type: 'node.create', payload: { kind: 'task', title: '实现用户登录接口', content: '目标:新增 POST /api/login 接口', position: { x: 100, y: 100 }, }, })); await sleep(1000); ws.send(JSON.stringify({ type: 'node.create', payload: { kind: 'file', title: '读取 src/auth/login.ts', content: 'export async function login() { /* auth 逻辑 */ }', position: { x: 300, y: 200 }, }, })); await sleep(1000); ws.send(JSON.stringify({ type: 'node.create', payload: { kind: 'status', title: '测试通过,等待人工审批', content: 'npm test 全部通过,共 12 个用例', position: { x: 500, y: 300 }, }, })); console.log('Agent 事件推送完成'); });4.5 前端画布页面
前端渲染层不需要多复杂,核心是监听 WebSocket 事件,把它们渲染成画布上的卡片。
<!-- 文件:public/index.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Murmell Demo Canvas</title> <style> body { margin: 0; background: #1e1f22; color: #d4d4d4; font-family: sans-serif; } #canvas { position: relative; width: 100vw; height: 100vh; overflow: hidden; } .node { position: absolute; min-width: 180px; max-width: 260px; background: #2d2d30; border: 1px solid #4c4c4e; border-radius: 8px; padding: 12px; font-size: 13px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); } .node h3 { margin: 0 0 6px 0; color: #8dc5e3; font-size: 14px; } .node pre { white-space: pre-wrap; color: #a8a8a8; margin: 4px 0 0 0; } .tag { display: inline-block; margin-top: 8px; padding: 2px 8px; border-radius: 10px; font-size: 11px; color: #fff; } .tag.task { background: #7a5b1f; } .tag.file { background: #4d6b5a; } .tag.status { background: #34567a; } </style> </head> <body> <div id="canvas"></div> <script> const canvas = document.getElementById('canvas'); function addNode(node) { const div = document.createElement('div'); div.className = 'node'; div.style.left = node.position.x + 'px'; div.style.top = node.position.y + 'px'; div.innerHTML = ` <h3>${node.title}</h3> <pre>${node.content || ''}</pre> <span class="tag ${node.kind}">${node.kind}</span> `; canvas.appendChild(div); } const ws = new WebSocket('ws://localhost:3000'); ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.type === 'canvas.snapshot') { msg.payload.forEach(addNode); } if (msg.type === 'node.create') { addNode(msg.payload); } }; </script> </body> </html>4.6 运行与验证
开启两个终端:
# 终端 1 node server.js # 终端 2 node agent-worker.js打开浏览器访问http://localhost:3000,你会在画布上看到 Agent 依次创建的三个节点。如果同时打开多个浏览器窗口,它们会保持实时同步。
这个原型虽然简单,但它已经具备了 Murmell 这类工具的核心链路:Agent 产生事件 -> WebSocket 同步 -> 画布实时渲染 -> 多端可见。
5. 与真实 Coding Agents 的集成思路
真实项目里,你不会只用一个模拟脚本来推送事件。Murmell 的真正价值,是接入真实的编码代理。这一节讨论常见的集成模式。
5.1 工具调用拦截模式
大多数编码 Agent 都支持工具调用(Tool Use)。你可以在 Agent 的工具执行层加一层“事件旁路”,也就是在调用read_file、write_file、run_command等工具时,额外向 Murmell 画布推送一条事件。
伪代码思路:
def on_tool_call(tool_name, payload): if tool_name == "write_file": canvas.push_event({ "type": "node.create", "kind": "file", "title": f"写入文件 {payload['path']}", "content": payload["content"][:200], # 只取摘要,避免画布过载 }) elif tool_name == "run_command": canvas.push_event({ "type": "node.create", "kind": "status", "title": f"执行命令 {payload['command']}", })这种模式的好处是侵入性低,不用改 Agent 的核心逻辑。
5.2 审批拦截模式
对于需要人工确认的高风险操作,比如删除文件、修改生产配置、批量迁移数据,可以通过画布实现“拦截式审批”。
实现思路:
- Agent 在准备执行高风险操作前,先向画布发起一个审批请求节点。
- 画布显示“等待审批”状态。
- 人类开发者点击“通过”或“驳回”。
- 结果回传给 Agent,Agent 再继续执行。
这种模式在真实场景中很关键。比如在自动化代码迁移或者大规模重构时,人工审批点可以显著降低误操作风险。
5.3 仓库级会话关联模式
每个画布会话可以关联一个 Git 仓库、一个分支,甚至一个具体的 PR。这样 Agent 产生的每个节点都可以关联到具体的代码提交。
在这类产品中,比较实用的设计是让每个画布节点携带 Git 元信息:
{ "id": "node_456", "kind": "file", "title": "修改 login.ts", "repo": "code-platform/user-service", "branch": "feat/login-api", "commit": "a3f4d5e6f7a8b9c0" }这样方便回溯:这个画布节点是在哪一次提交、由哪个 Agent 产生的,是否经过人工批准。
6. 常见问题与排查思路
这类云端画布工具在使用和自建过程中,经常会遇到下面这些问题。这里整理成表格,方便查阅。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 画布一片空白 | Agent 端没有连接到服务 | 检查 WebSocket 连接地址和 Agent 日志 |
| 节点出现重复 | Agent 重发同一事件 | 事件写入采用幂等方式,按事件 ID 去重 |
| 画布更新有明显延迟 | WebSocket 无法连接,前端轮询兜底 | 检查网络代理、防火墙,确认 WebSocket 路径 |
| Agent 推送大量节点,画布卡顿 | 单次同步节点过多 | 做节点压缩、分页加载,只渲染可视区域 |
| 两个 Agent 同时编辑同一节点,内容互相覆盖 | 客户端无冲突处理策略 | 增加节点级锁,或改用 CRDT 算法 |
| 前端显示状态和实际 Agent 状态不一致 | 状态事件丢失或乱序 | 服务端维护有序事件流,前端按 sequence 排序 |
| 打开多个浏览器标签页,部分页面不同步 | 重连后未重新拉取全量快照 | 连接建立后先推送 canvas.snapshot |
6.1 事件丢失问题
在 WebSocket 连接不稳定的场景下,事件可能丢失。缓解方式:
- 服务端为每条事件分配自增序号。
- 客户端记录最后一条序号,重连后从断点处补齐。
- 对敏感操作(审批、修改文件),使用“事件 + 确认”机制。
6.2 节点内容过大
Agent 读取一个大型文件时,如果直接把全文推到画布,会导致网络开销大、渲染卡顿。比较稳妥的办法:
- 节点只保存文件路径。
- 开发者在画布中点击节点时,再按需拉取文件内容。
- 或者只推送文件开头 N 字节 + 摘要。
6.3 权限边界模糊
如果画布已经接入了真实代码仓库,权限失控会直接导致源码泄露。务必做到:
- 访问画布必须先通过统一身份认证。
- Agent 推送的节点遵循“最小内容”原则。
- 内网部署时通过防火墙限制画布服务端口。
- 所有画布请求记录操作者 ID 与 IP。
7. 最佳实践与工程建议
7.1 事件模型先于 UI 设计
在搭建 Murmell 这类工具时,一定要先定义清楚事件模型再写 UI。建议用 JSON Schema 约束每一种事件类型。事件类型越规范,后续接入不同 Coding Agent 时成本越低。
7.2 幂等写入是基本要求
Agent 网络重试、断线重连,很容易导致同一事件被发送两次。服务端在写入画布节点时,必须做幂等。常见做法是使用eventId作为唯一键,重复写入时直接忽略。
7.3 画布与代码仓库分离存储
不要把画布节点直接存进代码仓库。画布有自己独立的数据库,代码仓库只存储源码。这样可以避免画布操作污染 Git 历史,也能避免非开发人员误操作仓库。
7.4 审计日志必须独立
所有 Agent 产生的画布操作,都应该有独立审计日志。尤其在高权限操作场景,比如“允许 Agent 推送代码到 release 分支”,必须能回答四个问题:谁(哪个 Agent)在什么时候、通过哪个会话、做了什么事情。
7.5 灰度发布与安全回滚
如果你在自建一个面向团队的工具,建议采用“会话级别灰度”方式:先让一个项目组试用,再逐步推广。每个画布版本都要支持快照回滚,避免一次错误的状态变更影响了多个正在运行的 Agent 任务。
7.6 对接 Agent 时的协议选择
如果 Agent 已经支持 MCP(Model Context Protocol),优先通过 MCP 接入画布。这样可以避免直接修改 Agent 源码,用一套统一的工具协议把 Agent 能力“暴露”给画布。
目前主流的编码 Agent 都在往 MCP 方向演进。Murmell 这类 canvas 工具如果做成 MCP Server,就可以低成本接入大量支持 MCP 的 Agent,这个方向值得重点观察。
8. 对使用者的建议
8.1 关注画布的“人工审批点”
在使用 Murmell 时,建议把画布审批点当作任务推进的阀门。不要让 Agent 连续执行超过 5 个步骤才创建一次审批点。步骤越短、审批越频繁,越容易在早期发现方向性错误。
8.2 注意画布中代码片段的敏感程度
画布可能会保存 Agent 读取过的代码片段。务必确认,你的代码仓库里没有把明文密码、密钥、Token 写入源码。如果有,接这种画布工具会加剧敏感信息扩散风险。建议先在团队里执行一轮密钥扫描。
8.3 小团队先用轻量模式
如果团队只有 3 到 5 人,不太需要一上来就部署一整套权限系统和审计平台。可以先让画布工作在“只读 + 审批”模式,Agent 产生的节点不能被普通成员编辑,所有写操作统一由负责人完成。这个模式对系统性能的要求低,也更容易被团队接受。
编码代理和人类开发者的协作界面,正在从“纯终端日志”走向“可视化画布”。Murmell 这个名字虽然还比较新,但它代表的“Collaborative Cloud Canvas”方向,很可能就是未来 AI 编程工具链条上重要的一环。如果你正在做 Agent 相关工作,花点时间研究这类画布工具的设计思路,一定会对如何构建更可控的 AI 编码流程有新的启发。