04-多窗口实战:点赞功能的并发编排全链路

73 阅读9分钟

多窗口实战:点赞功能的并发编排全链路

这篇解决什么问题

上一篇文章说清了多窗口编排的工程本质——隔离 > 压缩,以及 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:DTO1❌ 等 1窗口 A(接续任务 1)
任务 3:Service1、2❌ 等 1、2窗口 A(接续任务 2)
任务 4:Controller2、3❌ 等 2、3窗口 A(接续任务 3)
任务 5:前端组件 UI 绘制无✅ 可并发窗口 B(与任务 1 同时启动)
任务 6:前端接口定义 DTO 及页面调用接口2、5🟡 DTO 完成后即可窗口 B(接续任务 5)

依赖分析的本质就一句话:没有依赖的任务可以并发,有依赖的串在同一窗口。

关键并发点有两个:

  1. 任务 1(Model)和任务 5(前端 UI 绘制)可以同时启动——UI 绘制不需要任何后端产出,画界面不需要知道接口长什么样
  2. 任务 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 可以同时开始
前端接口定义等 DTODTO 是前后端的契约,契约一定,前端才能按契约定义接口类型和调用逻辑

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。


六、测试验证闭环

每个原子任务配套验证方案,做完即验,不等最后:

任务验收点
1Schema 正确 + 迁移生成成功
2DTO 类型完整 + 校验装饰器齐全 + 导出正确
3事务逻辑正确 + 点赞/取消都走通 + 计数一致
4路由可访问 + Guard 生效 + 参数校验正常
5组件渲染正常 + 动效流畅 + Props 类型正确
6接口类型与 DTO 对齐 + 乐观更新流畅 + 后端返回校正

全部通过 → 提交代码。


七、可复用的编排模板

把这个案例抽象成通用模板,下次拿到任何一组原子任务都能直接套:

步骤 1:画依赖图

逐个任务问:这个任务要读谁的产出? 把答案列成表格。

步骤 2:标记并发窗口

  • 无依赖的任务 → 可并发,分配到不同窗口
  • 有依赖的任务 → 跟着它的上游串在同一个窗口
  • 被多个下游依赖的产出 → 写入共享文档,让下游窗口直接读取

步骤 3:分配窗口

窗口类型职责上下文特征
Worker 窗口执行一条完整的依赖链纯净,只包含当前链路的知识
并发窗口依赖上游产出但可独立推进等依赖完成后启动,上下文隔离

步骤 4:执行 + /clear

  • 同一窗口内连续依赖的任务 → 不 clear,保持连贯
  • AI 跑偏 → 立即 clear,带四要素重新开始
  • 切换领域(后端→前端)→ 换窗口,天然隔离

一句话收束

多窗口编排的核心不是"开几个窗口",而是画对依赖图 + 分对窗口。依赖图决定谁能并发,窗口分配决定上下文纯净度。前端 UI 绘制往往没有后端依赖,可以和后端链路同时启动;DTO 是最关键的并发分叉点——契约一定,前后端各自开工。这套模板不依赖任何框架——手动多窗口、Subagents 都是在这个模板上的不同实现方式。