单计划内部的门控已经够了,还不够
当任务多了之后,我做了两件事:给每个计划加了自己的 task graph(任务图)和 finish gate(完成条件)。效果不错——单计划内部的依赖清晰了,不会出现"最后一个任务执行完了但整个计划没关闭"的情况。
但很快遇到了新问题:计划 A 的执行结果,会影响计划 B 能不能开始。而两个计划之间没有任何依赖声明。
举个例子:
- 计划 A:重构博客发布模块
- 计划 B:搭建新的内容发布工作流
它们分别属于不同的优先级队列,分别有各自的 finish gate。但计划 A 的输出——发布模块的新接口——是计划 B 的输入。计划 B 的团队并不知道计划 A 改了接口,直接用旧接口写了编排脚本。等计划 A 一上线,计划 B 的脚本全部报废。
这不是协作问题,是跨计划依赖没有被显式管理的架构问题。
为什么跨计划依赖容易被忽略
我仔细想了这个问题:为什么在单计划里能管好依赖,跨计划就管不好?
第一个原因是计划边界天然隔离了状态。 每个计划有自己的任务图、自己的输出目录、自己的完成条件。两个计划的管理者(无论人还是 AI)都不会主动去看另一个计划的内部状态。这不是不负责,而是边界设计本来就不鼓励跨计划窥探。
第二个原因是依赖关系不是功能性的,而是数据性的。 计划 A 和计划 B 之间可能没有任何功能上的交集,但 A 产出了一份配置格式文件,B 需要用它做输入。这种数据层面的依赖不会在计划评审时被注意到。
第三个原因是"开始前依赖"和"完成时依赖"之间的模糊。 计划 B 可能在计划 A 执行到一半时就可以启动了(部分依赖),还是必须等 A 全部完成才能启动(全依赖),或是等 A 完成某个里程碑就够了(里程碑依赖)。没有明确的描述,执行端只能猜。
设计:跨计划依赖红绿灯
我参照单计划里 task graph 的思路,给计划队列加了一层跨计划依赖关系,叫依赖门控(dependency gating)。
每一条跨计划依赖包含四个要素:
计划 B 依赖计划 A 时,需要声明:
1. 被依赖方(A)的哪个输出
2. 依赖方(B)需要这个输出的什么状态(完成 / 到达某个里程碑)
3. 验证方式(读 A 的输出文件 / 检查 A 的 finish gate 状态 / 调用 A 的验证脚本)
4. 依赖失效时的行为(阻塞 B / 警告但放行 / 降级到降级方案)
这四个要素到位之后,跨计划依赖就不需要人去"记住"了。系统会变成一个自动红绿灯:
- 红灯:A 还没到达指定状态,B 不能启动
- 黄灯:A 的部分输出可用,B 可以有限启动(比如先做架构设计,等 A 完成后再编码)
- 绿灯:A 的输出就绪,B 可以完整启动
分阶段依赖:不是非黑即白
实际使用中我发现,"等 A 完成"并不是唯一合理的依赖模式。有些依赖是分阶段的。
比如计划 B 需要在 A 完成后才能做集成测试,但不妨碍 B 先做方案设计和编码。如果必须等 A 全部完成才能开始 B,整个交付周期会被拉长。
所以我在依赖门控中加入了阶段标记:
B 依赖 A 的输出来做集成测试 → A 的"接口定义完成"里程碑即可
B 需要 A 的运行时配置来启动 → 等 A 的部署完成
B 只需要知道 A 的完成结果 → 等 A 的 finish gate 关闭
这样一来,B 可以在 A 执行到一半时就开始准备工作。跨计划依赖不再是"一一对应"的等待关系,而是多阶段、多粒度的协调机制。
依赖的传递效应
实际运行中还发现一个问题:依赖关系会沿着依赖链传递。 计划 B 依赖计划 A,计划 C 依赖计划 B。当计划 A 的输出发生变更时,受影响的不只是 B,还有沿着链传递下去的 C、D、E。
如果不处理这种传递效应,就会出现
计划 A 在推进中决定修改输出接口,原本是返回 JSON,改成了返回 XML。所有依赖 A 输出的计划 B、C、D 根本不知道这个变更,直到执行出错才发现。
解决这个问题的方法是依赖变更通知。当计划 A 的输出声明发生变化时,系统应当:
- 通知所有注册了"依赖 A 输出"的计划
- 标记相关依赖的状态为"pending_review"
- 等待依赖方确认或更新依赖条件后,再变回正常状态
这个机制听起来很简单,但它要求在计划 A 变更输出时,能自动查找到哪些计划依赖于它。这就需要依赖关系是双向可追溯的——不仅仅是 B 声明"我依赖 A",而是系统里能回答"A 被谁依赖着"。
下次我会怎么做
第一,每个计划的输出声明要显式化。 不是等别人来问"你输出了什么",而是在计划初始阶段就声明:"我将产出 X 文件、Y 配置、Z 接口。" 这对于跨计划依赖的自动发现至关重要。
第二,跨计划依赖不要写在计划正文里,而要写在队列的依赖配置层。 计划正文经常被修改和重读,依赖声明最好放在一个不会被任务重写覆盖的独立层。
第三,给依赖关系加一个"变更通知"机制。 不管是用消息通知还是定期扫描,需要让依赖链上的每个节点知道上游的变化。静默变更是不一致的源头。
第四,红绿灯要有超时和手动覆盖机制。 如果依赖方因为某种原因长期处于"pending"状态,不能无限阻塞被依赖方的后续任务。加入一个超时时间和管理员覆盖通道,允许在特殊情况下手动放行。
依赖图的拓扑排序问题
当计划数量超过五个之后,依赖关系不再是简单的链式结构,而会变成一张有向图。这就引入了拓扑排序的问题:如果依赖图里存在环,整个队列就会死锁。
我遇到过的真实案例:计划 A 依赖计划 B 的配置输出,计划 B 又依赖计划 A 的接口定义。两个计划都在等对方先完成,谁也动不了。这不是谁设计错了,而是需求本身就是循环的——A 需要B 的配置格式,B 需要 A 的接口契约。
解决方案是把循环依赖拆成阶段:先让 A 输出一个接口草案(milestone 1),B 基于草案开始工作并输出配置(milestone 1),然后 A 再根据 B 的配置完善接口(milestone 2)。这样循环就变成了螺旋迭代。但前提是依赖声明必须支持里程碑粒度,而不是只有 "完成/未完成"两个状态。
在依赖图检测上,我加了一个简单的环检测:每次添加新的跨计划依赖时,遍历依赖图检查是否存在环路。如果检测到环,不是直接拒绝,而是提示把循环部分拆解成里程碑级别的阶段性依赖。这比事后发现死锁要好得多。
依赖门控的运行时开销
一个容易被忽略的问题是:依赖门控本身也有运行时开销。每次启动一个计划之前,都要检查它的所有上游依赖是否就绪。如果依赖链很长(A→B→C→D),检查可能涉及多个计划的多个里程碑状态。
我的做法是缓存依赖检查结果。每个依赖状态的检查结果带一个 TTL(比如5分钟),在 TTL 内不重复检查。同时,上游计划状态变更时主动推送通知,让缓存及时失效。这样既保证了实时性,又不会让依赖检查成为启动瓶颈。
另外,我把依赖检查做成了幂等操作。无论检查多少次,只要上游状态没变,结果就是一样的。这意味着即使缓存失效后重试,也不会产生副作用。
依赖声明的成本与收益
坦白说,显式声明跨计划依赖是有成本的。每个计划需要在创建时就思考"我会产出什么、我需要什么",这增加了计划编写的前置工作量。如果团队小、计划少,这些声明可能看起来过度设计。
但我的经验是:当你觉得不需要跨计划依赖管理的时候,通常是因为计划数量还没到阈值。 一旦计划超过五六个,并且开始并行推进,隐式依赖就会像定时炸弹一样逐个引爆。到那时候再补依赖声明,成本远高于一开始就做。
所以我的建议是:从第一天起就写依赖声明,哪怕只有一个跨计划依赖。养成习惯的成本很低,事后补救的成本很高,这个账很容易算。
跨计划依赖看起来是一个调度问题。追到根上,它是一个数据一致性和变更管理问题。 把所有跨计划的数据交换和状态依赖显式化,就是不让任何两个计划在不知道对方的情况下碰同一个数据。