先记住这个答案
先检查任务标识和依赖引用,再用拓扑排序判断前置依赖图是否存在环。Kahn 算法反复处理入度为零的节点;若最终处理数少于总节点数,剩余部分受到环路阻塞。要定位真正的环,可继续使用带递归栈的 DFS 或强连通分量分析。修复时应回到产物语义,拆分先产出草稿再评审的阶段,不能随便删除依赖让调度继续。
- 未知依赖和重复标识应先作为输入错误处理
- 拓扑排序剩余节点包括环内节点及其受阻下游
- 合法反馈循环应显式建模状态、版本和退出条件
先确认边的方向与输入契约
约定依赖从前置任务指向使用它的任务,或者在节点中列出前置标识,整个系统必须一致。重复依赖可以按集合去重;重复节点标识则不能静默覆盖,否则一个任务可能凭空消失。依赖不存在的节点也应直接报错,而不是让它永久等待。
自依赖是最短的环,可以直接识别。一般环路需要遍历图判断,不能只检查两两互相引用,因为三个以上任务也可能形成闭环。下面示例先做基本输入校验,再维护入度和后继表,每条边只在建图与释放时处理有限次数。
function inspectPlan(tasks) {
const byId = new Map(tasks.map(task => [task.id, task]));
if (byId.size !== tasks.length) throw new Error('duplicate id');
const degree = new Map(tasks.map(task => [task.id, 0]));
const next = new Map(tasks.map(task => [task.id, []]));
for (const task of tasks) {
for (const dep of new Set(task.deps)) {
if (!byId.has(dep)) throw new Error('unknown dependency');
degree.set(task.id, degree.get(task.id) + 1);
next.get(dep).push(task.id);
}
}
const queue = tasks.filter(task => degree.get(task.id) === 0)
.map(task => task.id);
const order = [];
for (let head = 0; head < queue.length; head++) {
const id = queue[head];
order.push(id);
for (const child of next.get(id)) {
degree.set(child, degree.get(child) - 1);
if (degree.get(child) === 0) queue.push(child);
}
}
return { order, blocked: tasks.filter(task => degree.get(task.id) > 0)
.map(task => task.id) };
}
const result = inspectPlan([
{ id: 'research', deps: [] },
{ id: 'draft', deps: ['research', 'review'] },
{ id: 'review', deps: ['draft'] },
{ id: 'publish', deps: ['review'] }
]);
console.log(result.order.join(','));
console.log(result.blocked.join(','));查看输出与解释
research
draft,review,publishdraft 与 review 互相依赖;publish 只是它们的下游,也会出现在 blocked 中。用队列下标推进,在通常的 Map 操作代价假设下,时间为 O(V+E),空间为 O(V+E)。
定位真正需要修改的依赖
输出中 publish 不属于环,它只是等不到 review。因此不能把剩余节点全部叫做环成员。可以在剩余子图用 DFS 的当前访问路径发现回边,或用强连通分量找出互相可达的节点组;单节点分量还要检查是否存在自环。
面向用户的诊断最好展示一条闭合等待路径以及每条边等待的产物。例如草稿等待评审结论、评审又等待草稿,说明阶段定义混在一起。改为先生成初稿,再评审,再生成修订稿,可以保留业务要求并消除一次性任务间的互相等待。
不要把合理迭代误判成业务错误
评审后修改再评审是合理的运行循环,但要标明稿件版本、终止条件和迭代上限。一次性依赖图可展开有限轮次,或由外层状态机管理循环。不能仅因为工作流允许循环,就忽略某个阶段内部永远无法开始的前置依赖。
计划修改后应重新检查受影响结构,并核查已经运行的任务是否采用旧输入。自动修复可以提出拆分或替换依赖的候选方案,最终仍要验证它没有跳过必需检查。检测失败时保留图和错误路径,比让模型反复盲目重写整份计划更便于定位原因。
容易答错的地方
- 随便删一条边让图无环
- 无环不代表依赖正确。删除评审前置条件可能使未经检查的产物进入后续流程,必须根据业务产物重新建模。
- 用重试解决结构死锁
- 相同图和相同状态下重复调度不会产生缺失产物,应先修正依赖或为合法迭代建立可执行的初始状态。
面试官还会怎么问?
排序结果一定唯一吗?
不一定。多个无依赖关系的就绪节点可以交换顺序,仍满足所有前置关系;需要稳定输出时再增加明确的排序策略。
只想知道有没有环,需要找所有环吗?
不需要。拓扑处理数量就能判断,或用 DFS 找到第一条回边停止;详细诊断再进一步定位。针对“Agent 计划循环依赖检测与修复”,还应保留最小复现、预期结果和失败路径,避免只凭一次现象下结论。
依赖图正确就能保证任务成功吗?
不能,它只排除一类结构错误。输入质量、工具能力、资源冲突和业务验收仍需单独验证。针对“Agent 计划循环依赖检测与修复”,还应保留最小复现、预期结果和失败路径,避免只凭一次现象下结论。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。