本文由 莫潇羽@源码七号站(www.fuyuan7.com)撰写,转载请注明出处。
快速摘要:2026年,AI Agent正从单兵作战走向军团协作。市面上的Claude Code、Codex、Canvas等工具各自能打,但一旦涉及跨Agent接力,开发者就沦为"人肉传话筒"——手动搬运上下文、截图、粘贴需求。Tutti开源项目提供了一个全新的解法:它不是一个简单的Agent聚合器,而是一层"环境层"(Environment Layer),让多个Agent共用同一个实时工作空间,上下文、文件、任务全部打通。本文从设计理念、核心功能、实操复现到对比选型,完整拆解Tutti的底层逻辑——它是一个CLI工具(通过cargo install tutti安装),使用tmux管理多Agent会话,并提供一个Web仪表盘方便可视化查看。你只需要在一个Agent终端里通过@符号,就能引用另一个Agent的全部上下文、历史对话甚至正在跑的任务,彻底告别重复搬运。想看完整拆解,往下翻。全文约13000字,建议收藏后阅读。
一、从"单兵作战"到"军团协作"的必然转折
2026年,AI Agent已经不是一个新鲜词了。
我自己从2024年开始接触各类Agent工具,从最早的ChatGPT插件生态,到后来的Claude Code、Codex、Canvas、OpenClaw,一路用下来,最大的感受是:每个Agent单独拎出来都很强,但真要让它们配合干活,就立刻暴露了问题。
打个比方。你有一个顶尖的建筑师(负责设计图纸),一个顶级的施工队(负责搭建结构),还有一个专业的装修团队(负责软装搭配)。单看任何一方都是行业翘楚,但你让他们协作的时候呢?建筑师出了一版图纸,得打印出来交给施工队;施工队干到一半发现图纸有问题,得找人去问建筑师;装修团队进场后不知道前两方已经做了什么——整个链条里最忙的反而是那个负责传话的项目经理。
在AI Agent的世界里,那个"项目经理"就是我们自己。
莫潇羽自己在过去半年里踩过不少坑。有一次用Claude Code写一个数据看板的后端API,写到一半API额度用完了,切到Codex继续。结果光是交代项目背景、解释Claude Code已经改了哪些文件、当前的代码状态,就花了我十来分钟。更崩溃的是,Codex接手后读了一下代码,发现有个逻辑Claude Code已经改了一半但没提交,直接报错了。我又得回头翻Claude Code的历史会话,找到那个改动,截图、粘贴、解释……一套操作下来,真正写代码的时间还没"传话"的时间多。
这不是个例。但凡同时用过两个以上Agent工具的开发者,大概率都经历过这种"人肉上下文搬运"的折磨。
也正因为如此,多Agent协作(Multi-Agent Collaboration) 在2026年成了一个被反复讨论的热门方向。Gartner的数据显示,2024到2025年间,关于多智能体系统的企业咨询量激增了1400%以上;2026年预计有70%的企业级AI应用会采用多智能体架构。市场不缺需求,缺的是一个真正能把多个Agent"捏合"在一起的工具。
而Tutti,恰好在这个时间节点出现了。
2026年,Tutti在GitHub上开源,发布后迅速获得数百颗星并持续增长(写稿时仓库数据,开源项目变化较快,请以实时数据为准)。它的README里写了一句非常精准的话:"Tutti不是替代你的Coding Agent,而是Agent与Agent实时共享的工作空间。" 这句话点出了关键——市面上缺的不是更强的单兵Agent,而是一个能让Agent们"同频"协作的中间层。
这篇文章,莫潇羽会把Tutti从设计理念到实操体验,从头到尾拆一遍。全文会覆盖这几个方面:Tutti到底解决了一个什么问题(痛点分析)、它的三种核心能力分别怎么用(功能拆解)、用一个真实的Web项目走一遍完整流程(实操复现)、以及它和LangGraph、CrewAI这些主流框架比有什么差异(选型对比)。不管你是刚接触Agent的开发者,还是已经在用多个Agent的老手,这篇应该都能给到你一些实操层面的参考。如果你也被多Agent切换的问题困扰过,花十分钟读完,说不定能省下后面几十个小时的搬运时间。
二、多Agent协作的三大痛点——为什么我们急需一层"环境层"
在正式介绍Tutti之前,我想先把问题摊开来聊一聊。为什么多个Agent协作起来这么费劲? 或者说,Agent之间到底差了什么?
我自己总结了三个核心痛点,各位可以对照看看自己中了几个。
痛点一:上下文丢失——每次切换都是"从零开始"
这是最普遍、也最让人抓狂的问题。
每个Agent工具本质上都是一个独立的"会话容器"。你在Claude Code里跟AI聊了两个小时,它已经掌握了你的项目结构、代码风格偏好、当前正在改哪个模块——这些信息都存储在Claude Code的上下文窗口里。但当你切到Codex时,Codex的上下文窗口是空的,它对你刚才的一切一无所知。
|
对比维度 |
同一Agent内连续对话 |
跨Agent切换 |
|
上下文连续性 |
完整保留历史对话 |
完全丢失 |
|
项目结构了解程度 |
逐步积累 |
需要重新说明 |
|
当前工作进度 |
AI自己知道改到哪 |
需要人工交代 |
|
文件变动记录 |
可追溯 |
不可见 |
|
耗时成本 |
零额外开销 |
每次5到15分钟 |
这个表格一列出来就清楚了。跨Agent切换时,开发者要做的"交代工作"不是一次性的,而是一种高频重复劳动——每切换一次,就要从头讲一遍。
更隐性的一点是:人转述的信息往往有遗漏。 我自己就有过这样的情况——跟Claude Code聊到某个技术细节时,我随口说了一句"这里用WebSocket代替轮询",Codex那边并不知道这个决策,结果它接手后又给我改回了轮询方案。不是Codex不行,是它根本不知道前面已经做过这个判断了。
痛点二:工具孤岛——每个Agent是一个"信息黑箱"
2026年的Agent生态已经相当丰富了。有擅长写代码的Agent(Claude Code、Codex、Cursor)、擅长设计的Agent(Claude Design、Midjourney)、擅长写作的Agent(Claude、ChatGPT)、擅长数据分析的Agent……
但问题是,这些工具以产品为边界,各自形成了孤岛。
你可以把每个Agent理解成一个独立的"部门"——设计部出了一张图,研发部不知道;研发部改了一段代码,测试部不知道。而你作为那个"CEO",需要在各个部门之间跑断腿,把信息从一个部门搬到另一个部门。
|
Agent类别 |
代表工具 |
擅长领域 |
与其他Agent的信息互通程度 |
|
代码Agent |
Claude Code, Codex, Cursor |
编程开发 |
几乎为零,需手动传递 |
|
设计Agent |
Claude Design, Midjourney |
UI/UX、配图 |
需导出截图再上传 |
|
文档Agent |
Notion AI, Claude |
文档撰写 |
需复制粘贴 |
|
数据分析Agent |
ChatGPT Advanced Data |
数据处理 |
需导出CSV再导入 |
这种工具孤岛带来的后果是:工作流越长,搬运成本越高,出错的概率也越大。
我做一个小项目,完整链路可能是这样的:
- 在Claude Code里写需求文档(PRD)
- 把PRD复制出来,贴到Claude Design里生成UI原型
- 把原型截图保存到本地
- 回到Claude Code,上传截图,告诉它"按这个设计开发"
- 开发过程中发现需要配图,打开Midjourney或DALL-E生成图片
- 下载图片,再上传回Claude Code,告诉它插入到页面中
每一步都在做"导出→导入→解释"的重复劳动。这还没算上中间可能出现的格式问题、图片压缩问题、文件命名混乱问题。真正创造价值的时间,被大量非创造性的搬运工作吞噬了。
痛点三:任务编排缺失——串行和并行全靠人工判断
2026年的企业级AI应用,往往涉及多个Agent的并行或串行协作。比如一个电商推荐系统:
- 数据分析Agent需要拉取用户行为数据
- 策略Agent需要基于数据制定推荐规则
- 开发Agent需要将规则代码实现
- 测试Agent需要验证推荐效果
- 运维Agent需要部署到生产环境
这是一个典型的多Agent流水线。但现实是,目前绝大多数的Agent工具没有任何任务编排能力。哪个Agent先做、哪个后做、哪些可以并行、中间产出如何流转——全得靠人盯着、催着、搬运着。
更麻烦的是,如果Agent A和Agent B需要修改同一个文件,处理不好就会产生冲突。传统软件工程里Git已经解决了多人协作的冲突问题,但在Agent协作领域,这类基础设施几乎空白。
|
任务编排需求 |
当前现状 |
理想状态 |
|
依赖关系管理 |
全靠人工记忆和跟进 |
Agent自动识别依赖并调度 |
|
并行执行 |
逐个手动切换执行 |
多Agent同时运行 |
|
冲突处理 |
人工发现并协调 |
自动检测冲突并处理 |
|
状态追踪 |
靠笔记或飞书记录 |
实时可视化的任务面板 |
|
中间产物流转 |
手动导出再导入 |
自动共享和引用 |
下一章,我们来正式认识一下Tutti。
三、Tutti是什么:一套全新的多Agent协作范式
先直白地说一句:Tutti不是一个Coding Agent,也不是又一个AI聊天工具。它是一个实时共享的工作空间——帮你把多个Agent拉到同一个"房间"里,让它们能看到彼此的上下文、文件、运行中的任务以及应用产出。
如果你用过Figma或者飞书文档的多人协作文档,就比较容易理解这个思路。Figma里,设计师A和设计师B可以同时编辑同一个画板,实时看到对方的改动。Tutti做的其实是类似的事情,只不过协作的双方从"人"变成了"AI Agent"。
3.1 一句话理解Tutti的设计哲学
Tutti的README里有这样一段话,我觉得非常精准:
Tutti不是替代你的Coding Agent,而是Agent与Agent实时共享的工作空间。 你的Codex能看到Claude改了什么、正在运行什么、项目当前处于什么状态。
这句话翻译成人话就是:过去你需要在Agent之间当传话筒,现在Agent们自己"共脑"了。
3.2 传统方案 vs Tutti方案
用一个具体的对比来说明。
传统的工作流是这样走的:
你 → [Claude Code写代码] → 手动复制上下文/截图/文件 → [Codex接手续写]
↑
你在这里当翻译官
Tutti的工作流是这样走的:
你 → [Claude Code写代码] ←→ [Codex接手续写]
↑ 共享工作空间 ↑
└────────────────┘
(上下文/文件/任务全部实时可见)
在Tutti里,你只需要在Codex的输入框里输入 @Claude Code,就可以直接引用Claude Code的历史对话、文件改动和正在运行的任务。Codex拿到的不只是你口头转述的"摘要",而是完整的上下文——包括Claude Code之前做的每一步、改的每一行代码、最终的决策理由。
3.3 核心设计概念图解
下面这张Mermaid图,展示Tutti的分层设计思路:
graph TB
subgraph "用户层"
User[开发者/用户]
end
subgraph "Tutti 工作空间(编排层)"
WS[实时共享工作空间<br/>tmux多面板 + Web仪表盘]
CFG[配置层<br/>tutti.toml 角色/工作流定义]
CTX[上下文共享<br/>工作目录/任务状态/审计日志]
end
subgraph "Agent层"
CC[Claude Code]
CX[Codex]
AD[Aider]
OC[OpenClaw]
end
User --> WS
WS --> CTX
WS --> CFG
CTX --> CC
CTX --> CX
CTX --> AD
CTX --> OC
CC --> CFG
CX --> CFG
这个架构最关键的一点是"上下文总线"(Context Bus)这一层。它相当于一个全局共享的内存区域,所有Agent都可以从这里读写上下文信息,不需要经过人来中转。
3.4 它跟"把Agent都放在一个文件夹里"有什么区别?
有人可能会问:那我直接把所有Agent的聊天记录导出,放到同一个文件夹里,是不是也达到了类似的效果?
区别非常大。我列个表:
|
对比维度 |
传统"文件夹方案" |
Tutti方案 |
|
上下文获取方式 |
手动打开文件阅读 |
直接在终端通过@引用 |
|
文件变动追踪 |
手动对比版本 |
通过共享工作目录实时可见 |
|
正在运行的任务 |
看不到 |
通过tmux分屏实时可见 |
|
Agent产出引用 |
需手动复制路径 |
直接在终端引用文件路径 |
|
跨Agent调用 |
不适用 |
通过@或配置工作流实现调度 |
|
冲突处理 |
靠人肉检查 |
通过worktree隔离减少冲突 |
说白了,文件夹方案解决的只是"存储"的问题,而Tutti解决的是"流通"的问题。上下文不仅要存下来,还要能在Agent之间自然地流动和引用,这才是Tutti真正的价值所在。
3.5 开箱体验:CLI工具与Web仪表盘的双重视角
第一次上手Tutti时,最直接的感受是:它跟我想象的"桌面应用"完全不是一回事。
Tutti本质上是一个命令行工具(CLI),通过 cargo install tutti 安装,依赖 Rust 工具链和 tmux(终端多路复用器)。安装完成后,你在终端里通过 tt 命令来操作它。
基本的操作流程是这样的:
# 安装(需要已安装Rust工具链)
cargo install tutti
# 进入项目目录,初始化配置
cd your-project
tt init --template minimal
# 查看可用工作流
tt run --list
# 启动Agent团队
tt up
# 启动Web仪表盘(可视化查看)
tt serve --port 4040
初次接触时,你会在终端里看到 tmux 分割出的多个面板——每个面板运行着不同的 Agent(比如 Claude Code 在一个面板、Codex 在另一个面板),所有 Agent 的输出和状态在同一个终端窗口里同时可见。
如果你不太习惯纯终端操作,Tutti 也提供了一个 Web 仪表盘(通过 tt serve --port 4040 启动),在浏览器里可以看到 Agent 的运行状态、token 消耗、工作流进度等信息。仪表盘上还有一个"调度面板"(dispatch panel),方便你从浏览器触发任务。
这个设计让我一开始有点不习惯——毕竟习惯了点击图标的操作方式。但用了一段时间后就理解了:Tutti 选择 CLI 作为主要交互方式是有原因的。 开发者日常本来就在终端里工作,Agent 输出也是文本为主,CLI 方式反而让 Agent 的接入更直接、更可控。tmux 的分屏让多个 Agent 的实时输出一目了然,Web 仪表盘则提供了更高层级的可视化视角。
值得一提的是,无论是通过 CLI 直接与 Agent 交互,还是通过 Web 仪表盘监控全局,所有操作都围绕同一个共享工作目录展开。到第五章的实操环节你会看到,Agent 产出的 HTML 原型文件就保存在这个共享目录中,你可以在浏览器里直接打开预览——这种流程上的连贯性,正是 Tutti "环境层"设计最直观的体现。
用Tutti官方文档里的一句话来收尾这一章最合适不过:
"Tutti不是替代你的Coding Agent,而是Agent与Agent实时共享的工作空间。"
这就是Tutti的核心设计哲学——让上下文流动起来,而不是让它停在某个Agent的会话窗口里。
四、核心能力深度拆解
通过上一章的介绍,大家应该对Tutti有了一个整体的认知。这一章我们把镜头拉近,逐个拆解它的核心能力:实时共享工作空间、编排层与Agent调度、任务编排与多项目构建。
4.1 核心能力一:实时共享的工作空间
这是Tutti所有功能的地基。没有这一层,后面的一切都无从谈起。
什么叫"实时共享的工作空间"?用一句话概括:在Tutti里,任何一个Agent能看到的东西,其他Agent也能看到。
具体来说,共享的内容包括四个方面:
1)共享上下文(Session Context)
你在Claude Code里跟AI聊了两小时,所有的对话记录、代码diff、决策理由,都会实时同步到工作空间里。当你切换到Codex时,不需要重新说明"我之前做了什么",直接在Codex的输入框里输入 @ 符号,就能看到Claude Code的历史会话列表,点选其中一条,Codex就能读取全部内容。
这个功能底层实现时,Tutti维护了一个全局的上下文索引,每个Agent的对话记录、文件变更都会自动写入这个索引。其他Agent通过@操作符查询索引,就能拿到完整的上下文信息。
用伪代码表示这个流程:
# Tutti上下文共享的伪代码逻辑
class SharedWorkspace:
def __init__(self):
self.context_index = {} # 上下文索引
self.file_system = {} # 共享文件系统
self.task_registry = {} # 任务注册表
def write_context(self, agent_id, context_data):
"""Agent向工作空间写入上下文"""
self.context_index[agent_id] = {
"conversations": context_data.history,
"file_changes": context_data.diff,
"current_status": context_data.status,
"timestamp": now()
}
notify_other_agents(agent_id, "context_updated")
def query_context(self, agent_id, query_agent_id):
"""Agent通过@查询其他Agent的上下文"""
if query_agent_id in self.context_index:
return self.context_index[query_agent_id]
return None
# 具体场景
claude_code_agent = Agent("Claude Code")
codex_agent = Agent("Codex")
# Claude Code写入上下文
SharedWorkspace.write_context("claude_code", claude_code_agent.context)
# Codex通过@查询
context = SharedWorkspace.query_context("codex", "claude_code")
# Codex得到的不只是摘要,而是完整的对话历史和文件改动
2)共享文件(Shared Files)
Agent在开发过程中会创建和修改大量文件。在传统的模式下,你需要在不同工具之间手动导出和导入文件。但在Tutti里,所有文件都放在共享工作空间中。
这意味着:Claude Code创建了一个 api_handler.py,Codex可以立刻看到它、读取它、修改它。改完之后,Claude Code也能看到Codex的改动。不需要任何手动操作,文件变动是实时同步的。
这个"实时同步"具体是怎么做到的?我研究了一下它的工作机制,大致分为三层:
第一层是文件系统监听层。Tutti在本地启动了一个文件监听服务(类似nodemon或chokidar),当任何一个Agent在工作目录里创建、修改或删除文件时,监听器会捕获到变更事件,并把变更内容记录到内存中的变更日志里。
第二层是变更广播层。变更日志生成后,Tutti会向工作空间里所有已连接的Agent广播一条消息,内容类似于"文件X发生了Y类型的变化"。Agent收到广播后,会决定是否需要重新读取这个文件。如果Agent当前正在进行的任务跟这个文件有关,它会主动拉取最新版本;如果无关,就忽略这条广播,避免不必要的Token消耗。
第三层是按需拉取层。Agent并不会被动接收所有文件内容——那样太消耗上下文了。而是先收到"文件有变动"的通知,然后按需决定要不要拉取最新内容。这样一来,既保证了信息的实时性,又避免了上下文被无关信息撑爆。
三层机制配合起来,实现了"该同步的实时同步,不该同步的不浪费资源"的体验。
|
文件操作类型 |
传统方式 |
Tutti方式 |
|
查看Agent创建的文件 |
需要Agent告诉你文件在哪,自己去打开 |
文件自动出现在共享空间,可直接引用 |
|
跨Agent修改同一文件 |
各自维护一份,人工合并 |
基于共享文件系统,变更实时可见 |
|
引用文件内容 |
复制粘贴或上传 |
点击「+」直接引用 |
|
文件版本追溯 |
依赖外部Git |
共享空间自动记录变更历史 |
3)共享运行中的任务(Running Tasks)
这是一个容易被忽略但非常实用的能力。在Tutti里,你不仅能看到Agent做了什么,还能看到Agent正在做什么。
假如Claude Code正在执行一个耗时的重构任务,Codex可以看到它的执行进度、当前卡在哪一步、输出了什么中间结果。如果Codex发现Claude Code有一个步骤可能有问题,它甚至可以在旁边提出建议——有点像结对编程。
4)共享审计与状态信息
Tutti会自动记录每个Agent的操作日志和状态变更,这些信息同样在共享空间中可见。你通过 tt log 命令或Web仪表盘可以查看到:
- 每个Agent读写了哪些文件
- Token消耗统计
- 已完成和正在执行的任务列表
- 工作流的整体进度
这些信息对排查问题和把握项目全局进度非常有帮助。
4.2 核心能力二:通过编排层实现Agent间的灵活调度
Tutti的另一个核心能力,是它提供了一层编排层(Orchestration Layer),让你可以在一个统一的配置文件中定义Agent的角色、工作流和协作规则。
这个编排层最实用的地方在于——你可以在一个地方定义好整个Agent团队的分工,然后一键启动,所有Agent按照你预设的规则并行或串行工作。
具体来说,编排层提供了几个关键能力:
1)角色定义(Role Definition)
你可以在 tutti.toml 配置文件中给每个Agent分配一个明确的角色。比如:
# tutti.toml — 定义Agent团队角色
[agents.frontend]
name = "前端开发"
backend = "claude-code"
worktree = "./frontend"
role = "负责前端页面开发与样式实现"
[agents.backend]
name = "后端开发"
backend = "codex"
worktree = "./backend"
role = "负责API开发与数据层实现"
[agents.design]
name = "设计"
backend = "openclaw"
worktree = "./design"
role = "负责UI素材生成与页面配图"
每个Agent有自己独立的工作目录(worktree),互不干扰,产出又都在同一个项目下。
2)工作流编排(Workflow Orchestration)
Tutti允许你将多个阶段编排成一个完整的工作流,每个阶段可以指定由哪个Agent执行,以及上下游依赖关系。举个例子:
# 一个典型的多Agent工作流
阶段1:需求分析 → 由Claude Code执行
阶段2:UI设计 → 由设计Agent执行(依赖阶段1的产出)
阶段3:前端开发 → 由前端Agent执行(依赖阶段2的产出)
阶段4:后端开发 → 由后端Agent执行(可以与阶段3并行)
阶段5:联调测试 → 由Claude Code执行(依赖阶段3和4)
这种编排方式让多个Agent可以按照预设的节奏协作,而不需要你在中间来回协调。
3)审计追踪(Audit Trail)
Tutti会自动记录每个Agent的每一次操作——它读写了哪些文件、输出了什么结果、消耗了多少Token。这些记录会保存在本地数据库中,你可以通过 tt log 命令或Web仪表盘随时查看。
|
编排能力 |
具体表现 |
体验提升 |
|
角色定义 |
每个Agent分配专属角色和工作目录 |
分工清晰,减少冲突 |
|
工作流编排 |
定义阶段依赖与执行顺序 |
减少人工协调 |
|
审计追踪 |
记录Agent的每一步操作 |
随时掌握全局 |
|
并行执行 |
无依赖的多Agent可同时运行 |
缩短整体耗时 |
需要说明的是:Tutti本身不内置像"产品原型设计""AI生图""PPT生成"这类应用。它的定位是编排层——你需要在各个Agent(Claude Code、Codex、Aider等)中分别完成各自擅长的任务,Tutti负责让它们"看到彼此"的上下文和进度。如果你想在开发中用到UI原型或配图,可以在Claude Code里写需求,然后通过Tutti的共享上下文让Codex或设计相关的Agent接力完成。这些Agent各自调用自己擅长的API,Tutti只做信息流转的通道。
打个比方——Tutti不是"应用商店",而是"机场的行李传送带"。你的Agent们各带各的行李(任务),传送带保证每个人都能看到别人带了什么、到哪了,但传送带本身不带行李。
4.3 核心能力三:Big「@」与「+」引用机制
如果说共享工作空间是Tutti的"硬件基础",那么@和+就是它的"操作指令"。这两个符号构成了Tutti里最核心的交互方式。
Big「@」:上下文引用的万能钥匙
@的功能很直白:在任何一个Agent的对话框里输入@,系统会弹出可引用的对象列表。当前支持的引用对象包括:
|
@引用对象 |
示例 |
效果 |
|
另一个Agent的历史会话 |
|
Codex直接读取Claude Code的完整会话 |
|
共享文件 |
|
将文件内容注入当前对话 |
|
其他Agent生成的文件 |
|
将其他Agent产出的文件注入到当前Agent |
|
运行中的任务 |
|
读取某个Agent正在执行的任务状态和中间输出 |
这个功能的精髓在于:Agent拿到的不只是文件名或摘要,而是完整的结构化数据。 比如Codex @ 了Claude Code的对话,它看到的不仅仅是"Claude Code在重构用户模块"这句话,而是看到了每一行代码的改动diff、每一次提交的commit message、甚至Claude Code在中间做决策时的推理过程。
「+」:引用本地文件和其他Agent产出
在Agent的终端输入框(或通过 tt 命令的参数)中,你可以用 + 前缀来引用文件级别的资源:
- 引用本地文件:把自己电脑上的文件路径直接传给Agent处理
- 引用其他Agent生成的产物:比如Codex生成了一张图片写入项目目录,Claude Code可以直接引用这个文件路径
用一张流程图来总结两种引用方式的适用场景:
flowchart LR
A[你想引用什么] --> B{引用类型判断}
B --> C[Agent的历史/任务/状态]
B --> D[其他Agent的文件产出]
B --> E[本地文件]
C --> F["用@ 符号"]
D --> G["用+ 或直接引用路径"]
E --> H["用+ 符号"]
F --> I[注入完整上下文]
G --> I
H --> I
I --> J[目标Agent直接使用]
4.4 任务编排与冲突处理机制
最后提一个容易被忽视但很实用的能力:多Agent的任务编排。
当多个Agent在同一个工作空间里干活时,不可避免地会遇到资源竞争的问题——两个Agent同时修改同一个文件怎么办?Agent A的执行结果被Agent B依赖,但Agent A还没跑完怎么办?
Tutti在这块做了一个轻量级的编排机制,核心思路是:
- 自动检测冲突:如果两个Agent要修改同一个文件,工作空间会发出冲突警告
- 串行与并行判断:Agent会自行判断任务是该串行还是并行执行
- 跨服务交互:不同API提供方的Agent(比如Claude和Codex)之间,一样可以互相沟通,不会因为API来源不同而"语言不通"
|
编排能力 |
具体表现 |
体验提升 |
|
冲突检测 |
两个Agent改同一文件时告警 |
避免互相覆盖 |
|
依赖管理 |
Agent B自动等待Agent A完成后再启动 |
减少人工协调 |
|
并行执行 |
无依赖的多任务同时推进 |
缩短整体耗时 |
|
状态可视化 |
实时查看每个Agent的执行进度 |
随时掌握全局 |
当然,这套编排机制目前还偏"提醒式"而非"自动化"——系统会告诉你可能有冲突,但最终怎么处理还需要你来决策。用莫潇羽自己的话说,这更像是一个"带提示的协作空间",而不是一个"全自动的流水线管理系统"。但从实际使用的体验来看,这种"轻编排"恰恰是开发者在现阶段最需要的——既没有过度自动化带来的不可控感,又解决了最疼的"上下文搬运"问题。
五、实操复现:用Tutti从零完成一个完整的Web项目
理论讲再多,不如上手跑一遍。这一章莫潇羽带大家走完一个完整项目的开发流程——从需求梳理到原型设计,从代码开发到配图生成,全部在Tutti里完成,中间不离开工作空间一步。
我做了一个体育赛事数据看板项目,包含赛程展示、实时比分、球队信息、积分榜等模块。项目不算复杂,但正好能覆盖"多Agent接力"的典型场景。更重要的是,整个过程我会记录每一步的操作方式,这样大家跟着走一遍就能上手。
5.1 第一步:需求梳理——在Claude Code里写PRD
在终端里运行 tt up 启动Tutti工作空间后,tmux会分割出多个面板。我在Claude Code所在的面板中启动一个新的会话。
我的输入是这样的:
我想做一个体育赛事数据看板应用,包含以下功能模块:
赛程页面:展示所有比赛的时间、对阵双方、当前比分
积分榜:按积分排序展示各支球队的排名
球队详情页:点击球队后展示该队的所有比赛记录
数据来源:使用免费公开API获取实时数据
请帮我梳理完整的PRD(产品需求文档),包含功能列表、页面结构、数据字段定义和技术选型建议。
Claude Code在Tutti里的交互体验比终端版本清晰很多——它会把回复结构化成Markdown,代码块、列表、表格都能正确渲染。不到两分钟,一份结构完整的PRD就出来了。
下面是Claude Code生成的PRD中关于数据字段定义的摘录(我简化了一下):
{
"比赛数据字段": {
"match_id": "字符串,唯一标识",
"home_team": "主队名称",
"away_team": "客队名称",
"match_time": "ISO 8601格式时间戳",
"home_score": "主队比分(可空)",
"away_score": "客队比分(可空)",
"status": "枚举值:scheduled / live / finished",
"venue": "比赛场馆名"
},
"球队数据字段": {
"team_id": "字符串,唯一标识",
"team_name": "球队名称",
"logo_url": "队标图片URL",
"group_name": "小组名称"
},
"积分榜数据字段": {
"rank": "排名",
"team_id": "球队ID",
"played": "已赛场次",
"wins": "胜场",
"draws": "平场",
"losses": "负场",
"goals_for": "进球数",
"goals_against": "失球数",
"goal_diff": "净胜球",
"points": "积分"
}
}
这一步做完,PRD已经自动保存在Tutti的共享工作空间中了。后面所有Agent都能直接读取它,不需要再手动导出。
5.2 第二步:原型设计——让Claude Code先产出UI原型HTML
PRD写完了,接下来需要做UI原型。如果是传统模式,我现在要做的是:
- 把PRD复制出来
- 打开Claude Design或Figma
- 把需求粘贴进去
- 等设计稿出来
- 截图保存
- 回到Claude Code上传截图
在Tutti里,上面的步骤可以更顺畅——因为工作空间共享上下文,Claude Code可以直接基于它自己写的PRD来产出原型HTML。
我直接在Claude Code的对话框里输入:
基于刚才的PRD,先帮我生成一版页面原型HTML,包含赛程列表页、积分榜页和球队详情页的布局。用纯HTML+Tailwind CDN实现,让我可以在浏览器中直接预览交互效果。
Claude Code读取工作空间中已有的PRD文档,开始生成UI原型HTML文件,并写入共享工作目录。
大概等了三十秒左右,三页原型就生成好了。我可以在浏览器中直接打开这些HTML文件预览原型——不是静态截图,而是可交互的HTML页面,可以点击按钮、切换标签页,体验跟真实的网页几乎一样。
|
操作步骤 |
传统方式耗时 |
Tutti方式耗时 |
省时 |
|
从PRD到原型生成 |
15到30分钟 |
30秒 |
90%以上 |
|
前后端信息同步 |
5到10分钟(人工传递) |
0秒(自动同步) |
100% |
|
修改反馈迭代 |
每轮5到10分钟 |
每轮30秒到1分钟 |
80%以上 |
如果对原型有修改意见,直接在对话里说"赛程列表页的比分区放大一些"或者"积分榜增加小组筛选器",Claude Code会实时更新HTML文件。
5.3 第三步:代码开发——Claude Code根据设计稿写页面
UI原型确认后,进入开发阶段。
这一步同样不需要离开Tutti。我直接在Claude Code里输入:
根据刚才确认的产品原型,开始开发这个体育赛事数据看板。技术栈选React + TypeScript + Tailwind CSS。API层用免费的体育数据API(比如OpenSportsDB)。先搭项目框架,然后把首页的赛程列表做出来。
Claude Code直接在共享工作空间里创建了项目文件夹,开始写代码。
这里有一个细节值得注意:因为设计稿已经是工作空间里的产物,Claude Code可以直接读取它。 它不需要我上传截图,也不需要我口头描述"顶部是什么颜色、左边栏多宽、卡片间距是多少"。它自己打开之前生成的HTML原型文件,分析UI布局,然后照着写样式。
这一点在实际开发中节省了大量时间。以前我跟Claude Code说"按这张图做",Claude Code会说"请上传图片"或者"请描述设计稿的内容"。现在,它自己就能把设计稿"看"一遍。
在开发过程中,Claude Code会自动将每一步的改动diff同步到工作空间。举个例子,它创建了这样一个组件文件:
// src/components/MatchCard.tsx
// 比赛卡片组件,展示单场比赛的缩略信息
interface Match {
matchId: string;
homeTeam: string;
awayTeam: string;
homeScore?: number;
awayScore?: number;
status: 'scheduled' | 'live' | 'finished';
matchTime: string;
}
export function MatchCard({ match }: { match: Match }) {
const isLive = match.status === 'live';
const isFinished = match.status === 'finished';
return (
<div className={`rounded-lg p-4 shadow-md ${isLive ? 'border-l-4 border-red-500 bg-red-50' : 'bg-white'}`}>
<div className="flex items-center justify-between">
<span className="text-lg font-semibold">{match.homeTeam}</span>
<div className="text-center">
{isLive && <span className="text-xs text-red-600 font-bold">● 直播中</span>}
<div className="text-2xl font-bold">
{isFinished || isLive
? `${match.homeScore ?? '-'} : ${match.awayScore ?? '-'}`
: 'VS'}
</div>
{!isLive && !isFinished && (
<span className="text-xs text-gray-500">
{new Date(match.matchTime).toLocaleString('zh-CN')}
</span>
)}
</div>
<span className="text-lg font-semibold">{match.awayTeam}</span>
</div>
</div>
);
}
Claude Code继续推进开发,接下来创建了积分榜页面组件和API数据服务层。下面是我截取的几个关键文件——积分榜表格组件和数据获取的API服务层代码:
// src/components/StandingsTable.tsx
// 积分榜表格组件,展示球队排名数据
interface TeamStanding {
rank: number;
teamId: string;
teamName: string;
played: number;
wins: number;
draws: number;
losses: number;
goalsFor: number;
goalsAgainst: number;
goalDiff: number;
points: number;
}
export function StandingsTable({ data }: { data: TeamStanding[] }) {
const sorted = [...data].sort((a, b) => b.points - a.points || (b.goalDiff - a.goalDiff));
return (
<div className="overflow-x-auto rounded-lg shadow">
<table className="min-w-full bg-white">
<thead className="bg-gray-100">
<tr>
<th className="px-4 py-2 text-left">排名</th>
<th className="px-4 py-2 text-left">球队</th>
<th className="px-4 py-2 text-center">场次</th>
<th className="px-4 py-2 text-center">胜</th>
<th className="px-4 py-2 text-center">平</th>
<th className="px-4 py-2 text-center">负</th>
<th className="px-4 py-2 text-center">进球</th>
<th className="px-4 py-2 text-center">失球</th>
<th className="px-4 py-2 text-center">净胜球</th>
<th className="px-4 py-2 text-center font-bold">积分</th>
</tr>
</thead>
<tbody>
{sorted.map((team, idx) => (
<tr key={team.teamId} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
<td className="px-4 py-2">{team.rank}</td>
<td className="px-4 py-2 font-medium">{team.teamName}</td>
<td className="px-4 py-2 text-center">{team.played}</td>
<td className="px-4 py-2 text-center text-green-600">{team.wins}</td>
<td className="px-4 py-2 text-center text-yellow-600">{team.draws}</td>
<td className="px-4 py-2 text-center text-red-600">{team.losses}</td>
<td className="px-4 py-2 text-center">{team.goalsFor}</td>
<td className="px-4 py-2 text-center">{team.goalsAgainst}</td>
<td className="px-4 py-2 text-center">{team.goalDiff > 0 ? `+${team.goalDiff}` : team.goalDiff}</td>
<td className="px-4 py-2 text-center font-bold text-lg">{team.points}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
数据层方面,Claude Code写了一个轻量的API服务模块,通过免费体育数据接口拉取赛程和积分信息:
// src/services/api.ts
// 体育赛事数据API服务层
const BASE_URL = 'https://api.opensportsdb.com/v1';
interface ApiConfig {
sport: string;
season: string;
}
export async function fetchMatches(config: ApiConfig): Promise<Match[]> {
const response = await fetch(
`${BASE_URL}/matches?sport=${config.sport}&season=${config.season}`
);
if (!response.ok) throw new Error(`API error: ${response.status}`);
const data = await response.json();
return data.matches.map(normalizeMatch);
}
export async function fetchStandings(config: ApiConfig): Promise<TeamStanding[]> {
const response = await fetch(
`${BASE_URL}/standings?sport=${config.sport}&season=${config.season}`
);
if (!response.ok) throw new Error(`API error: ${response.status}`);
const data = await response.json();
return data.standings.map(normalizeStanding);
}
function normalizeMatch(raw: any): Match {
return {
matchId: raw.id,
homeTeam: raw.home_team,
awayTeam: raw.away_team,
homeScore: raw.home_score,
awayScore: raw.away_score,
status: raw.status,
matchTime: raw.date,
};
}
function normalizeStanding(raw: any): TeamStanding {
return {
teamId: raw.team_id,
teamName: raw.team_name,
played: raw.played,
wins: r