多窗口实战:点赞功能的并发编排全链路
这篇解决什么问题
上一篇文章说清了多窗口编排的工程本质——隔离 > 压缩,以及 Subagents / Agent Teams 的能力边界。
这篇解决一个更实际的问题:拿到一组原子任务后,怎么判断哪些能并发、怎么分配窗口?
用博客项目的"点赞功能"走一遍完整链路:依赖分析 → 窗口分配 → 逐窗口执行。每一步给出可复用的操作模板。
一、起点:6 个原子任务
"用户可以给文章点赞,再点一次取消,显示点赞数"——这个需求经过五步拆解法后,拆成 6 个原子任务,后端链路 Model → DTO → Service → Controller:
点赞功能
├── 任务 1:ArticleLikes Model + 联合唯一索引
├── 任务 2:ToggleLikeRequestDto / ToggleLikeResponseDto
├── 任务 3:ArticleService.toggleLike(事务:关系增删 + 计数更新)
├── 任务 4:ArticleController.toggleLike(Guard + 参数校验)
├── 任务 5:前端 LikeButton 组件 UI 绘制
└── 任务 6:前端接口定义 DTO 及页面调用接口
这些任务不是拍脑袋拆的——五步拆解法的前三步(顶层解耦 → 三大锚定 → 模块内流程拆解)确保了拆解质量,第四步(原子任务颗粒度)产出了这 6 个任务。(五步拆解法的完整过程详见另一篇文章)
每个任务满足原子化标准:单目标、明确输入、可独立开发测试、预计对话不超过 30 条。
单窗口串行也能跑,但 6 个任务在同一个上下文里滚,到了任务 5 切前端的时候,后端的 Prisma Model、DTO 细节、Service 事务逻辑全在上下文里——它们对前端组件没用,但占着 token 位,还可能误导 AI。
多窗口并发的动机不是"快",是纯净。
二、依赖分析:画图比开窗口先
多窗口编排的第一步不是开几个终端,而是画依赖图。依赖图决定了谁能并发、谁必须等。
2.1 分析依赖关系
逐个任务问一个问题:这个任务要读谁的产出?
| 任务 | 读取谁的产出 | 被谁读取 |
|---|---|---|
| 任务 1:Model | 无(独立创建) | 任务 2(DTO 引用 Model 字段)、任务 3(Service 操作 Model) |
| 任务 2:DTO | 任务 1(Model 字段结构) | 任务 3(Service 返回 DTO)、任务 4(Controller 参数 DTO)、任务 6(前端按 DTO 定义接口类型) |
| 任务 3:Service | 任务 1(Model)、任务 2(DTO) | 任务 4(Controller 调用 Service) |
| 任务 4:Controller | 任务 2(DTO)、任务 3(调用 Service) | 无 |
| 任务 5:前端组件 UI 绘制 | 无(纯 UI 绘制,不涉及接口定义) | 任务 6(接口接入需要在 UI 组件基础上进行) |
| 任务 6:前端接口定义 DTO 及页面调用接口 | 任务 2(DTO → 接口类型)、任务 5(UI 组件已就绪) | 无 |
2.2 标记并发可行性
| 任务 | 依赖 | 可并发 | 并发窗口 |
|---|---|---|---|
| 任务 1:Model | 无 | ✅ 可并发 | 窗口 A |
| 任务 2:DTO | 1 | ❌ 等 1 | 窗口 A(接续任务 1) |
| 任务 3:Service | 1、2 | ❌ 等 1、2 | 窗口 A(接续任务 2) |
| 任务 4:Controller | 2、3 | ❌ 等 2、3 | 窗口 A(接续任务 3) |
| 任务 5:前端组件 UI 绘制 | 无 | ✅ 可并发 | 窗口 B(与任务 1 同时启动) |
| 任务 6:前端接口定义 DTO 及页面调用接口 | 2、5 | 🟡 DTO 完成后即可 | 窗口 B(接续任务 5) |
依赖分析的本质就一句话:没有依赖的任务可以并发,有依赖的串在同一窗口。
关键并发点有两个:
- 任务 1(Model)和任务 5(前端 UI 绘制)可以同时启动——UI 绘制不需要任何后端产出,画界面不需要知道接口长什么样
- 任务 2(DTO)完成后,窗口 A 继续推进 Service → Controller,窗口 B 同时开始任务 6——DTO 就是前后端的契约,契约一定,前端就能按契约定义接口并接入页面
三、窗口分配:两个窗口的编排方案
依赖图画完,窗口分配自然浮出水面:
┌──────────────────────────────────────────────────────┐
│ 窗口 A(后端链路) │
│ 任务 1: Model → 任务 2: DTO → 任务 3: Service → 任务 4: Controller │
│ 上下文:纯后端,Prisma + NestJS │
└──────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────┐
│ 窗口 B(前端链路) │
│ 任务 5: LikeButton UI 绘制 → 任务 6: 接口定义 DTO + 页面调用 │
│ 上下文:React 组件 + DTO 定义,零后端业务细节 │
│ 启动条件:任务 5 无依赖,可与窗口 A 同时启动 │
└──────────────────────────────────────────────────────┘
3.1 为什么这么分
| 分配逻辑 | 说明 |
|---|---|
| 后端链路归一个窗口 | Model → DTO → Service → Controller 是强依赖链,同一上下文连贯性好 |
| 前端 UI 绘制无依赖,立即启动 | 画界面不需要后端产出,任务 5 和任务 1 可以同时开始 |
| 前端接口定义等 DTO | DTO 是前后端的契约,契约一定,前端才能按契约定义接口类型和调用逻辑 |
3.2 并发时间线
时间 ──────────────────────────────────────────→
窗口 A: [1 Model] → [2 DTO] → [3 Service] → [4 Controller]
窗口 B: [5 UI 绘制] → ↑ DTO 完成 → [6 接口定义 + 页面调用]
两个并发分叉点:
- 启动时:任务 1 和任务 5 无依赖,两个窗口同时开工
- DTO 完成后:窗口 A 继续 Service → Controller,窗口 B 从 UI 绘制进入接口定义
四、逐窗口执行:提示词 + /clear 策略
每个窗口内的执行,仍然遵循原子化开发的规则:四要素提示词 + 按需 /clear。
4.1 窗口 A:后端链路
任务 1:ArticleLikes Model + 联合唯一索引
| 要素 | 内容 |
|---|---|
| 目标 | 新增点赞关系的数据库模型 |
| 需求功能 | 新增 ArticleLikes 模型(id, article_id, user_id, created_at),联合唯一索引 @@unique([article_id, user_id]),Articles 模型添加 likes 字段 |
| 完成标准 | npx prisma migrate dev 迁移成功,无类型错误 |
| 约束 | Model 用 PascalCase,数据库字段用下划线命名 |
任务 2:DTO(接续任务 1,不 /clear)
| 要素 | 内容 |
|---|---|
| 目标 | 创建点赞接口的请求/响应数据结构 |
| 需求功能 | ToggleLikeRequestDto(articleId + 校验装饰器),ToggleLikeResponseDto(articleId, likes, isLiked),统一导出 |
| 完成标准 | DTO 类型完整,校验装饰器齐全,tsc 编译无错误 |
| 约束 | 禁止 any,可选字段加 @IsOptional,数字字段加 @Type |
任务 2 完成后,通知窗口 B 可以开始任务 6。
任务 3:Service 事务(接续任务 2,不 /clear)
| 要素 | 内容 |
|---|---|
| 目标 | 实现点赞/取消的核心业务逻辑 |
| 需求功能 | 查询 ArticleLikes 是否存在记录,已点赞→删除+计数减1,未点赞→创建+计数加1 |
| 完成标准 | 点赞/取消都走通,关系表和计数始终一致 |
| 约束 | 必须在 Prisma $transaction 中执行,保证原子性 |
任务 4:Controller(接续任务 3,不 /clear)
| 要素 | 内容 |
|---|---|
| 目标 | 新增点赞/取消点赞的 API 端点 |
| 需求功能 | POST toggle-like 端点,UseGuards(RemoteJwtAuthGuard),从请求获取 currentUserId,参数用 ToggleLikeRequestDto |
| 完成标准 | 路由可访问,Guard 鉴权生效,Swagger 文档正确显示 |
| 约束 | Controller 不写业务逻辑;userId 从 Guard 获取,禁止前端传参 |
任务 1→2→3→4 在同一窗口连续推进,上下文关联,AI 正常。如果某个任务跑偏,立即 /clear,只带该任务的四要素重新开始——不要在污染的上下文里纠偏,清除重来比修修补补快得多。
4.2 窗口 B:前端链路
任务 5:前端 LikeButton 组件 UI 绘制(可与任务 1 同时启动)
| 要素 | 内容 |
|---|---|
| 目标 | 绘制点赞交互组件的 UI 部分 |
| 需求功能 | LikeButton 组件 UI 绘制(红心图标、点赞数展示、点击动效),Props 预留 isLiked, likes, articleId, onToggle |
| 完成标准 | 组件渲染正常,点击动效流畅,Props 类型显式定义 |
| 约束 | 使用 useObserver 包裹渲染;Props 类型必须显式定义;暂不接入接口调用,只做 UI |
任务 6:前端接口定义 DTO 及页面调用接口(DTO 完成后 + 任务 5 完成后)
| 要素 | 内容 |
|---|---|
| 目标 | 定义前端接口类型并接入页面调用 |
| 需求功能 | 按后端 DTO 定义前端 ToggleLikeRequest/Response 类型,实现 api.article.toggleLike 调用,在 LikeButton 中接入接口调用(乐观更新 + 后端校正) |
| 完成标准 | 接口类型与后端 DTO 一致,乐观更新流畅,后端返回后状态校正正确 |
| 约束 | 接口类型必须与后端 DTO 严格对齐;禁止组件内直接使用 axios;未登录点击跳转登录页 |
窗口 B 的上下文只有 React 组件 + DTO 定义,零后端业务细节。AI 不会因为看到 Prisma $transaction 的讨论而把事务逻辑混进前端组件。
4.3 /clear 策略总结
| 时机 | 是否 /clear | 原因 |
|---|---|---|
| 窗口 A:任务 1→2→3→4 | ❌ 不 clear | 后端链路上下文关联,AI 正常 |
| 窗口 A:任何任务跑偏 | ✅ 立即 clear | 在污染上下文纠偏不如清除重来 |
| 窗口 B:任务 5→6 | ❌ 不 clear | 前端链路上下文关联,UI 到接口接入连贯 |
| 窗口 B:任务跑偏 | ✅ 立即 clear | 同理,清除重来 |
核心原则:/clear 的判断标准是上下文是否被污染,而不是任务是否完成。
五、模式对比:同一个需求,三种执行策略
把点赞功能用三种模式跑一遍,差异一目了然:
| 维度 | 单窗口串行 | 手动多窗口 | Subagents |
|---|---|---|---|
| 上下文纯净度 | 50%(6 个任务全在同一个上下文) | 100%(物理隔离) | 100%(独立 context window) |
| AI 出错率 | 高(后端细节干扰前端判断) | 最低(零跨域污染) | 最低(零跨域污染) |
| 并发能力 | 无 | 有(手动开窗口) | 有(框架调度) |
| 协调成本 | 零(但代价是上下文污染) | 中(手动分配) | 低(框架自动调度) |
| 可靠性 | 稳定(但不纯净) | 稳定(人兜底) | 稳定(官方能力) |
| 适合场景 | 简单需求、3 个任务以内 | 有明确依赖图的复杂需求 | 同左,且愿意让框架接管调度 |
注意一个细节:手动多窗口和 Subagents 的纯净度相同,差异只在"谁来做调度"。手动多窗口你就是 Orchestrator,Subagents 里框架帮你做 Orchestrator。
六、测试验证闭环
每个原子任务配套验证方案,做完即验,不等最后:
| 任务 | 验收点 |
|---|---|
| 1 | Schema 正确 + 迁移生成成功 |
| 2 | DTO 类型完整 + 校验装饰器齐全 + 导出正确 |
| 3 | 事务逻辑正确 + 点赞/取消都走通 + 计数一致 |
| 4 | 路由可访问 + Guard 生效 + 参数校验正常 |
| 5 | 组件渲染正常 + 动效流畅 + Props 类型正确 |
| 6 | 接口类型与 DTO 对齐 + 乐观更新流畅 + 后端返回校正 |
全部通过 → 提交代码。
七、可复用的编排模板
把这个案例抽象成通用模板,下次拿到任何一组原子任务都能直接套:
步骤 1:画依赖图
逐个任务问:这个任务要读谁的产出? 把答案列成表格。
步骤 2:标记并发窗口
- 无依赖的任务 → 可并发,分配到不同窗口
- 有依赖的任务 → 跟着它的上游串在同一个窗口
- 被多个下游依赖的产出 → 写入共享文档,让下游窗口直接读取
步骤 3:分配窗口
| 窗口类型 | 职责 | 上下文特征 |
|---|---|---|
| Worker 窗口 | 执行一条完整的依赖链 | 纯净,只包含当前链路的知识 |
| 并发窗口 | 依赖上游产出但可独立推进 | 等依赖完成后启动,上下文隔离 |
步骤 4:执行 + /clear
- 同一窗口内连续依赖的任务 → 不 clear,保持连贯
- AI 跑偏 → 立即 clear,带四要素重新开始
- 切换领域(后端→前端)→ 换窗口,天然隔离
一句话收束
多窗口编排的核心不是"开几个窗口",而是画对依赖图 + 分对窗口。依赖图决定谁能并发,窗口分配决定上下文纯净度。前端 UI 绘制往往没有后端依赖,可以和后端链路同时启动;DTO 是最关键的并发分叉点——契约一定,前后端各自开工。这套模板不依赖任何框架——手动多窗口、Subagents 都是在这个模板上的不同实现方式。