W7-Day2|Supervisor 模式:让一个 Agent 负责调度,多个 Worker 负责执行
昨天主要是在画 Supervisor 模式的图,今天开始真正把它写成代码。
今天的 Demo 是一个 Supervisor + 3 个 Worker 的多 Agent 流程:
| |
三个 Worker 分工:
| |
最重要的不是“有三个 Agent”,而是:
Worker 负责执行,Supervisor 负责决定下一步。
所以实际运行不是一条固定流水线:
| |
而是一个循环:
| |
Worker 干完以后必须重新回到 Supervisor。
一、先看今天的 State
今天的 State:
| |
在 MessagesState 自带的 messages 之外,又增加两个字段:
| |
这三个字段的职责并不一样。
messages:数据流
它保存整个任务的上下文。
例如:
| |
Worker 每次都是从这里读取前面的工作结果。
next:控制流
例如:
| |
表示:
下一步执行 writer。
所以 next 是给图的控制逻辑看的,而不是业务内容。
rounds:安全控制
Supervisor 是循环:
| |
所以必须记录轮数,防止模型一直循环。
二、为什么要有两个 LLM 变量
代码:
| |
当前它们使用的是同一个模型,但职责不同:
| |
Worker 的任务是:
| |
Supervisor 的任务是:
| |
虽然都是 LLM,但从系统设计上已经是两个不同角色。
三、MEMBERS:为什么专门维护一个名单
| |
这个列表相当于 Supervisor 的“花名册”。
后面很多代码都依赖它:
| |
所以它最好成为:
单一事实来源。
以后增加:
| |
就应该优先改这里,而不是到程序各处手写名字。
四、Route:Supervisor 怎么把“思考结果”交给程序
接下来:
| |
这里解决的是一个核心问题:
模型说完“下一步该谁”以后,程序怎么可靠地拿到这个答案?
不能让模型随便返回一句:
| |
而是约束成:
| |
其中真正影响图运行的是:
| |
而 reason 主要用于调试。
因此这里形成了一条链:
| |
五、为什么最后用 json_mode
最开始写:
| |
但实际跑 DeepSeek 时,默认路径触发了 json_schema,返回:
| |
后来又试了:
| |
结果变成:
| |
最后通过探针验证,当前环境真正可用的是:
| |
所以今天这里真正学到的并不是“DeepSeek 不支持结构化输出”。
而是:
同一个结构化输出接口,底层可能走完全不同的协议路径。
可以画成:
| |
所以以后遇到这类兼容问题,先写一个最小 Probe,确认模型实际支持什么,不要凭印象猜。
六、Worker 为什么用工厂函数创建
这部分源码初看有一点绕:
| |
它其实是在做一件事:
把“创建 Worker”这件事封装成一个模板。
然后:
| |
最终得到三个不同的 Worker:
| |
它们本质上结构类似,只是:
| |
所以这里的工厂函数主要是为了:
避免重复写三遍几乎一样的 Worker 创建代码。
七、三个 Worker 为什么职责不同
Prompt:
| |
工具:
| |
于是职责就很清楚:
| |
这里实际上已经有一个很清楚的数据加工链:
| |
Supervisor 只是负责控制这个加工链什么时候进入下一阶段。
八、真正执行 Worker 时,State 是怎么进去的
Worker 的核心代码:
| |
这里特别容易误解。
它不是:
| |
而是:
| |
所以 Worker 真正拿到的核心数据是:
当前 State 中的消息历史。
如果此时 State 是:
| |
那么 writer 就可以根据这些内容继续工作。
九、Worker 干完以后为什么还要重新包装一次消息
Worker 内部 Agent 最后得到:
| |
接着:
| |
这一步非常重要。
因为 Worker 内部的最终结果不能只停留在它自己的 Agent 里。
它必须进入 LangGraph 的 State。
于是:
| |
同时增加:
| |
于是 Supervisor 下一轮就知道:
| |
而不是只能看到一条匿名消息。
十、这里可以第一次把“逻辑流”和“数据流”放在一起看
现在完整看一次 researcher:
逻辑流
| |
这是:
决定谁执行。
数据流
| |
这是:
决定执行时拿什么,执行后留下什么。
两条流最后重新汇合:
| |
这是今天我认为理解 Supervisor 最重要的一张图。
十一、Supervisor 节点到底在做什么
源码核心:
| |
可以拆成四步。
第一步:计算轮数
| |
第二步:检查是否超限
如果已经超限:
| |
连 LLM 都不再调用。
这是为了避免已经知道要结束了,还白花一轮模型费用。
第三步:把“Supervisor 的规则 + 当前消息历史”交给 LLM
| |
模型看到:
| |
第四步:把模型决定写回 State
| |
所以 Supervisor 这一轮的作用其实非常简单:
读取 State → 决定下一步 → 修改 State。
十二、条件边:模型只负责“说去哪”,图负责“真的去”
路由函数:
| |
然后:
| |
完整过程其实是:
| |
所以一个非常值得记住的边界是:
LLM 做决策,Graph 执行决策。
模型不是直接调用:
| |
它只是在 State 中留下:
| |
然后 LangGraph 根据图结构真正执行 writer。
十三、为什么 Worker 必须回 Supervisor
这里:
| |
实际上是整个模式的循环核心。
形成:
| |
如果没有这组回边:
| |
那么 Worker 做完以后,就没有人继续判断下一步。
所以 Supervisor 模式真正的图形是:
| |
直到:
| |
十四、把一次完整运行串起来
现在假设模型做出了一个比较理想的路径:
| |
状态变化可以粗略理解为:
初始
| |
Supervisor 第一次
| |
researcher 执行
| |
Supervisor 第二次
读取:
| |
决定:
| |
writer 执行
| |
Supervisor 第三次
决定:
| |
reviewer 执行
| |
Supervisor 最后决定
| |
于是:
| |
整个过程中:
| |
越来越丰富。
而:
| |
负责控制当前应该去哪。
这就是今天最核心的:
数据流在 State 中积累,控制流围绕 State 动态前进。
十五、为什么要加 MAX_ROUNDS
Supervisor 本质上是动态循环。
例如 reviewer 可能说:
| |
于是:
| |
如果没有限制,模型可以一直绕下去。
所以:
| |
相当于:
给模型的自主决策设置一道硬上限。
而且今天特意做了 fallback 模式:
| |
让流程进入:
| |
这样才能真正证明兜底逻辑生效,而不是只看代码说“应该能生效”。
十六、今天的一个重要调试经验:两次运行不能拿来互相证明
之前的思路是:
| |
然后:
| |
再拿两者比较。
问题是:
它们根本不是同一次运行。
LLM 是动态的,所以即使:
| |
也不能把两个独立运行当成同一个实验。
最后改成:
| |
于是一次运行同时得到:
| |
这样才能比较:
| |
这个思路其实比 Supervisor 本身还通用:
验证动态系统时,尽量让观察数据来自同一次实验。
十七、今天还有一个特别隐蔽的测试坑:空集合
比如:
| |
如果:
| |
那么:
| |
于是你会产生一种错觉:
“检查通过了。”
实际上:
根本没有检查。
所以正确做法是:
| |
先证明:
| |
再检查数据是否合法。
这就是今天说的“真空真”。
十八、验证器也可能看错地方
今天还有一个反例。
原本检测工具是否真的被调用,写成:
| |
结果:
| |
但实际上工具真的调用了。
因为:
| |
走的是:
| |
而:
| |
才会进入:
| |
也就是:
| |
这是两条完全不同的数据通道。
所以后面才改成:
| |
两个证据一起看。
这个例子让我今天对测试又多了一层认识:
验证器本身也必须建立在正确的数据通道上。
十九、今天最终形成的理解
以前看到这种代码,容易先被很多 API 吓到:
| |
但今天把它还原成系统以后,其实就是:
| |
所以今天我真正理解的,不只是:
“Supervisor 可以调度多个 Agent。”
而是:
Supervisor 是一个不断读取 State、修改 State,并依据 State 决定下一跳的动态控制器。
而整个系统可以分成两条主线:
| |
这两条流最后都通过 State 汇合。
以后再看类似的多 Agent 代码,我觉得可以先问自己两个问题:
第一:谁决定下一步?
第二:上一阶段产生的数据,是通过什么字段传给下一阶段的?
先找到这两个答案,源码就不会再是一堆孤立的 API。
完整 Demo
下面保留今天最终版本的核心代码。
为了方便之后复习,完整 Demo 放在文章最后;省略 import、包安装和断言代码,正文重点放在运行机制和代码结构。
| |