最近用 Go 写了一个多 Agent 协作框架:Planner 拆解任务 → 多专家 Worker 并行执行 → Critic 审查 → Reviewer 评分重试 → 汇总交付。听起来很顺,但第一次真实运行就踩了两个结构性 bug,每个都让结果完全不可信。
架构设计
整体流程:
用户任务
↓
Planner ------ LLM 拆解为 N 个子任务,每个指定专家角色
↓
Worker × N ------ goroutine 并行执行
↓
Critic ------ 审查其他 Worker 的产出
↓
Reviewer ------ 对每个结果打分(1-10),未通过自动重试
↓
汇总输出
角色池有四个:general(通用)、writer(写作)、coder(代码)、critic(审查)。LLM 用的是 DeepSeek,所有 Worker 共享同一个 API key,只是 system prompt 不同。
Bug 1:角色映射键名不匹配
Planner 拆解任务后,给审查任务分配的角色名是 "critic"。但 Worker 初始化时,角色池 map 的键名写的是 "reviewer2":
go
// 错误:planner 分配 "critic",但 pool 里没有这个键
pool := map[string]Worker{
"general": &GeneralWorker{},
"writer": &WriterWorker{},
"coder": &CoderWorker{},
"reviewer2": &CriticWorker{}, // ← 键名不匹配
}
Go 的 map 查找不到键时不会报错,直接走 fallback 到 pool["general"]。运行时日志显示 [4] General-Worker (完成)------critic 任务用通用工人的身份去审查,system prompt 完全不对,质量可想而知。
修复: 把键名改成 "critic",和 Planner 分配的角色名对齐。教训是角色名是 Planner 和 Worker 之间的契约,应该用常量而不是手写字符串。
Bug 2:审查者看不到被审正文
这是更隐蔽的问题。所有 Worker 在 goroutine 里并行执行,每个 Worker 的 prompt 只包含自己的 task.Title 和 task.Description。
Critic 的任务描述是"审校文章技术准确性与表达",但它的 prompt 里没有 writer 写的正文。拿到的输入就是:
任务:审校文章技术准确性与表达
没有正文。Critic 只能输出一段审查报告框架,开头就声明"由于未提供文章内容,无法进行具体审查"。Reviewer 给它打了 3 分。
问题根源:并行执行导致 Worker 之间信息隔离。Critic 必须在 Writer 完成后才能审查,但如果全部并行,它执行时 Writer 还没写完。
修复:两阶段执行
把 Worker 执行改为两阶段:
markdown
第一阶段:所有非审查任务并行执行(goroutine + WaitGroup)
↓ 等待全部完成
第二阶段:审查任务串行执行,prompt 注入其他 Worker 的实际输出
核心改动是给审查任务传额外上下文:
go
func (w *CriticWorker) RunWithContext(ctx context.Context, task Task,
tasks []Task, otherResults []Result) error {
prompt := task.Description + "\n\n以下是其他子任务的实际产出," +
"请基于这些内容逐条审查:\n"
for _, r := range otherResults {
prompt += fmt.Sprintf("\n## 子任务 %s 产出\n%s\n", r.TaskID, r.Content)
}
return w.callLLM(ctx, task, prompt)
}
同时加了一个判断函数,自动识别哪些任务是审查类:
go
func isReviewTask(t Task) bool {
if t.AssignedTo == "critic" || t.AssignedTo == "review" {
return true
}
keywords := []string{"审查", "审核", "校对", "评估", "review", "critique"}
for _, kw := range keywords {
if strings.Contains(t.Title, kw) || strings.Contains(t.Description, kw) {
return true
}
}
return false
}
RunWorkers() 改为:先用 goroutine + WaitGroup 跑所有非审查任务,等全部完成后,再用 RunWithContext 串行跑审查任务,把其他任务的实际输出拼进 prompt。
修复后的运行效果
ini
📋 拆解为 4 个子任务:
[1] 梳理核心概念 → general
[2] 编写示例代码 → coder
[3] 撰写文章正文 → writer
[4] 审校技术准确性 → critic
⚡ [Worker] 并行执行中...
✅ [1] General-Worker (完成)
✅ [2] Coder (完成)
✅ [3] Writer (完成)
✅ [4] Critic (完成)
🔍 [Reviewer] 审查结果...
🔄 有 1 个任务未通过,开始重试...
🔄 [2] 重试第 1 次 (上次得分 6)
✅ [1] score=9
✅ [2] score=7 ← 重试后通过
✅ [3] score=8
✅ [4] score=9 ← critic 现在能看到正文了
统计:4 通过 / 0 未通过 / 4 总任务
对比修复前:critic 从 score=3(空转说"看不到原文")变成 score=9(逐条定位语法错误、反模式、API 过时问题),coder 首次 6 分重试到 7 分通过。
小结
两个 bug 的教训:
- 角色名是契约。Planner 和 Worker 之间的角色映射必须严格对齐,Go map 查不到键不会报错只会 fallback,这种静默失败最危险。
- 并行不等于最优。有依赖关系的任务(审查者依赖被审查者的产出)不能盲目并行。两阶段执行牺牲了一点并行度,但保证了信息完整。
项目已开源,包含完整代码、单元测试和 GitHub Actions CI: github.com/IYABAO/mult...