结论:别自己写DAG引擎,除非仅运行5个节点且无跨进程、重试、状态查询需求;否则将反复崩溃于toposort栈溢出、atomic.Value误用、循环检测报错不具体三大问题。

直接说结论:别自己写 DAG 引擎,除非你只跑 5 个节点、不跨进程、不重试、不查状态——否则你会在 toposort 栈溢出、atomic.Value 误用、循环检测报错不具体这三件事上反复崩溃。
拓扑排序必须用 Kahn 算法,不是 DFS
AI 工作流常有上百节点,DFS 递归做拓扑排序极易栈溢出;Kahn 算法用入度表 + 队列,天然适合并发调度。错误写法:toposort(node, visited) 递归调用;正确做法是维护 indegree map[string]int 和 queue []string,每次 pop 入度为 0 的节点,更新其下游入度。Goflow 和 Eino 都用 Kahn,不是巧合——它可中断、可分片、可加 context 超时。
节点状态不能存全局 map,得用 atomic.Value
并发执行多个任务时,map[string]NodeStatus 直接读写会 panic。必须用 atomic.Value 包一层:
var statusStore atomic.Value
statusStore.Store(make(map[string]NodeStatus))
// 更新时:
old := statusStore.Load().(map[string]NodeStatus)
new := make(map[string]NodeStatus)
for k, v := range old {
new[k] = v
}
new["task-a"] = NodeStatus{State: "running"}
statusStore.Store(new)
漏掉 copy 这一步,所有 goroutine 共享同一底层数组,状态就乱了。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
循环检测必须在加载阶段完成,且报错要带路径
DAG 加载时就要跑一遍环检测,运行时再检等于埋雷。检测逻辑不是“有没有环”,而是“哪条边导致环”。Eino 报错是:cycle detected: node 'validate' → 'enrich' → 'validate';而网上抄来的代码只报 graph has cycle,你得手动 trace 一百个边。实现建议:用 DFS 记录当前路径 path []string,回溯时拼出完整环路字符串,一并返回。
注册节点必须显式声明,否则 panic 是默认行为
像 engine.Register("llm_call", &LLMNode{}) 这种注册不是可选配置,是强制契约。Goflow 和 Eino 都会在 workflow.Load() 时遍历所有节点类型,发现未注册的 Type 就直接 panic。这不是 bug,是防错设计——避免 YAML 里写错名字却静默跳过。你写 type: "llm_invoke" 却没注册,它不会尝试 fallback,而是立刻崩给你看。
真正难的从来不是怎么连边,而是怎么让失败的任务能被人工看见、能断点续跑、能在超时后自动降级——这些都压在状态持久化和边条件表达式上,而不是图结构本身。

















