lab05 · 多智能体与编排(对应教程 Part 7)
墨叔 vs 老哥 对话讨论式讲解。本 lab 是
main.py的配套说明,跑之前先读这一节,跑完再回来看【思考题】。
0. 这个 lab 在演示什么
老哥:前面 6 个 lab 都是”一个 agent 转一圈”,这一节怎么突然就多 agent 了?
墨叔:因为现实里很多任务大到一个 agent 扛不住、杂到它顾不过来。单 agent 只有一个循环、一张上下文纸条、一套工具。当它要”既取数、又分析、又画图、又写报告”时,那条纸条会被撑爆,注意力也会被撕碎。这一节我们上多智能体,而且只讲最像你后端日常的那个姿势——Orchestrator-Worker(总控 + 工人)。
本 lab 跑出的完整链路:
用户 → 总控(orchestrator)
│ 拆解成子任务(任务看板)
│ 调用 dispatch 工具派活
├──→ worker: data_fetcher(独立上下文 run_loop,取数)
├──→ worker: analyst (独立上下文 run_loop,分析/画图)
│ (各自隔离,A 崩不影响 B)
│ 回收每个 worker 的"摘要"
└→ 汇总成最终报告
lab05/multiagent/main.py 把这条链路完完整整跑了一遍,而且是 mock 零依赖、零 API key 就能跑。重点看输出里的分层缩进轨迹:[orc] 是总控层,[w:data_fetcher] / [w:analyst] 是工人层——一眼就能看出”谁在调度、谁在干活”。
1. 什么时候需要多 agent——先别急着上(呼应 Part 7.1)
老哥:那是不是只要任务一复杂,就该拆多 agent?
墨叔:恰恰相反。绝大多数你以为”需要多 agent”的场景,单 agent 加一点工程技巧就够了。多 agent 是药,不是饭。先判断单 agent 的四道天花板有没有被真正顶到:
- 上下文窗口被撑爆——所有素材堆在同一张纸条。先试上下文裁剪(摘要/滑动窗口),别急着拆。
- 任务异构——让一个循环既当码农又当律师,prompt 变一锅粥。这是”该拆”的强信号。
- 需要并行——子任务互不依赖,单 agent 串行太慢。注意:隐藏依赖的”伪并行”是灾难。
- 需要隔离——有些”脏操作”(试毒输入、探索未知代码)别污染主上下文。
墨叔给你一把尺:先问单 agent 加裁剪能不能扛住 → 再问是不是真异构 → 再问子任务真能并行不 → 最后问有没有必须隔离的脏活。四条里显著命中一两条,才值得上多 agent。这和你”一个 Go 服务要不要拆微服务”的节奏一模一样——不会因为它”有点忙”就立刻拆。
2. Orchestrator-Worker:网关 + 微服务的翻版(呼应 Part 7.2)
老哥:OW 到底怎么分工?
墨叔:把它想成你后端的网关 + 微服务就通了。
- Orchestrator(总控/包工头):自己不干细节活,只做四件事——拆解目标、派活给 worker、回收结果、汇总成答案,外加失败兜底。它那张纸条里装的不是”干活细节”,而是”任务清单 + 各 worker 摘要”,所以不容易被脏细节撑爆。
- Worker(工人/微服务):每个 worker 是独立上下文的 agent 循环,有自己的 system prompt、自己的工具集。只关心”派给我的那一件子任务”,干完回一段摘要。
代码里三件事务必对齐:
- 上下文隔离:
run_worker每次都新建一个run_loop+ 自己的MockLLM实例,worker 之间互不共享消息。这就是天然的故障域拆分——你用recover兜 goroutine、用熔断兜下游,本质同一件事。 - 工具集隔离:
data_fetcher只有query_sales,analyst只有make_chart。不共享巨型工具箱,既省 prompt 体积,又降低”检索 worker 跑去执行 shell”的概率——对应最小权限原则(least privilege)。 - 摘要回流:worker 回给总控的是
out["final"]摘要,不是满屏原始输出。否则总控窗口又被灌爆,隔离白拆。“拆得开”还得”汇得简”,这是 OW 能成立的硬约束。
反模式预警:别把总控写成”超复杂硬编码总控”(worker 退化成傻执行器),也别写成”太弱的伪总控”(worker 各自为政拼不到一块)。甜区是:总控聪明在调度,worker 聪明在干活。
3. 主流框架对比:LangGraph / AutoGen / 自研(呼应 Part 7.3)
老哥:那我直接用什么框架?
墨叔:三个有代表性的,看它们”控制流是否显式”:
| 框架 | 世界观 | 控制流 | 适合 | 坑 |
|---|---|---|---|---|
| LangGraph | 把流程画成”图”(节点+边) | 显式声明:当前在哪个节点、下一步走哪条边都可查 | 分支多、需 checkpoint/恢复、要可观测 | 抽象有学习成本,易过度设计 |
| AutoGen | 多角色”靠聊天协作” | 隐式涌现:流程靠消息自然推进 | 探索性原型、多角色快速碰撞 | 难终止、难观测、易陷入无效循环 |
| 自研 | 自己写 orchestrator 循环 | 你完全掌控 | 嵌现有 Go 系统、流程其实不复杂 | 持久化/并发/可观测全得自己造 |
老哥:那选型尺是什么?
墨叔:就一条——框架省下的工程成本,是否大于它引入的抽象/学习成本。流程简单、要嵌生产,自己写 orchestrator 更干净(本 lab 就是);流程复杂、要任意节点可恢复,上 LangGraph。但无论如何,框架只解决”编排的机械部分”,解决不了”worker 聪不聪明、工具 schema 好不好”——地基(心智模型+工具+上下文)还得你按前面几章死磕。新手先拿图框架练手逼自己显式化,熟了再把高频简单那部分自研掉。
4. 多 agent 的协作成本——别只看收益,先看账单(呼应 Part 7.4)
墨叔:这是最猛的一盆冷水。多 agent 的四大隐性成本,本质就是你后端分布式系统的经典难题换皮:
- token 爆炸——N 个 agent 是 N 张独立嘴,每份 system prompt/工具清单重复喂,5 万 token 的任务拆 5 个 worker 可能变 30 万。等于”加机器分担负载,结果每台都加载全套依赖”。
- 错误传播——worker 错了,orchestrator 只看摘要、不知道它错,把错当事实汇总,错误被合法化、放大化。单 agent 的错当场闭环,多 agent 的错在传递链上。
- 调试地狱——面对 orchestrator + N 个 worker 各自的黑盒,像查跨五个微服务的分布式 bug,而且 agent 输入输出是自由文本,没有结构化日志好过滤。
- 隔离致信息丢失——拆开了就拼不回去,worker A 的关键线索没进摘要,B 就瞎猜。这是隔离的必然代价吗?不是,是”摘要传递”的代价——结构化传递 + orchestrator 显式搬运必要上下文能补。
老哥:那怎么驯服?
墨叔:用你后端的分布式功力——最小必要上下文、显式传递、全链路追踪、可信度校验关卡、保留退回单 agent 的退路。最重要一条:上线前先小样本量化,挑 10 个真实任务分别用单/多 agent 跑,记下 token、延迟、成功率。如果多 agent token 是 4 倍、延迟只降 20%、成功率没提升——那就是负优化,趁早砍。把”值不值”从玄学变成数字,是你作为工程师的底线。
5. 运行方式
# 默认 mock,零依赖零 API key,直接跑
python main.py
# 真实模式:仅总控走真实 LLM(需 OPENAI_API_KEY),worker 仍是可替换 stub
export OPENAI_API_KEY=sk-xxx
AGENT_MODE=real python main.py预期看到的分层轨迹(节选):
—— 总控拆解看板 ——
① data_fetcher : 取华东区 Q3 销售数据并摘要
② analyst : 基于数据给结论并生成趋势图
[orc][1] think 模型要求调用: dispatch({'subtasks': [...]})
└─ [派活] 启动 worker='data_fetcher',子任务: 取华东区 Q3 销售数据并摘要
[w:data_fetcher][1] think 调工具: query_sales({'region': '华东'})
[w:data_fetcher][2] think 说: 华东区 Q3 营收 1200 万...
└─ [回收] worker='data_fetcher' 返回摘要: ...
└─ [派活] 启动 worker='analyst' ...
[orc][2] think 模型说: 【最终报告】...
[orc] 是总控层,[w:xxx] 是工人层,缩进一眼分出层级——这就是”分层轨迹”。
【思考题】
- 单 agent 的”工具报错被模型当场自救”和多 agent 的”错误传播”相比,为什么后者更难兜?你后端对付”故障扩散”的哪几招能平移过来?
- “上下文隔离导致信息丢失”是隔离的必然代价吗?有没有办法既隔离、又不丢关键信息?类比你后端怎么在微服务之间传上下文。
- 如果让你把本 lab 的串行派活改成”可并行派活”,代码和总控的”任务看板”要补哪些东西?又该警惕 Part 7.4 里说的哪种成本暴涨?
【参考答案】
- 单 agent 里工具报错,模型在同一个循环、同一张纸条里立刻看到并自救,误差就地闭环;多 agent 里错误发生在 worker 内部,worker 只回”摘要”,可能把错误粉饰或漏报,orchestrator 拿到的是已被加工的信息,丢了原始报错信号,只能带着错往下汇总——错误被合法化、扩散化。后端对付故障扩散的三招可直接平移:① 熔断/降级(worker 错就标记失败,别污染下游);② 重试+幂等(orchestrator 对失败 worker 重试);③ 契约校验(orchestrator 收到 worker 结果先校验结构/可信度再采用)。核心是”别让上游脏数据被下游当事实默收”,加一道显式校验闸门。
- 不是隔离的必然代价,而是”摘要传递”的代价。隔离只是物理分开窗口,丢信息是你在传递时只传了摘要。解法有三:① 结构化传递而非自由散文——worker 返回带”关键产物字段 + 证据指针”的 JSON,orchestrator 按需再把原始细节取回(类比微服务返回 DTO 但保留详情查询接口);② orchestrator 主动搬运必要上下文——派活时把 B 需要的 A 产物显式塞给 B;③ 共享只读存储(黑板/共享记忆)——各 worker 把关键线索写进公共区,需要时自取,而非全塞进对话。这完全对应你后端”微服务间用共享缓存/事件总线传上下文,而不是靠口头协调”:隔离的是执行,共享的是必要状态。
- 要补三处:① 任务看板加状态机——每项子任务标
待派/进行中/已完成/失败与parallel标志,总控按依赖图调度(无依赖才并发);② 并发执行——本 lab 的dispatch现在是逐个run_worker,要改成”无依赖的 worker 并发启动”(类比go func()+WaitGroup,并注意取消传播);③ 超时与兜底——每个 worker 调用加timeout,卡死就标记失败再决定重试/跳过。要警惕的成本是 token 爆炸 + 协调轮次税:并行确实省墙皮时间,但 N 个 worker 各自重铺 system prompt/工具清单会让 token 总量放大数倍;而且 worker 回的摘要”不够好”时,总控得再派一轮澄清,每轮都是真金白银的 token。所以并行前务必先量化:省下的延迟是否抵得过暴涨的 token 与协调开销。