这是《AI Agent 进阶:手搓 Multi-Agent 系统》的第 2 篇。
今天不讲抽象概念,直接搭一个 Orchestrator,让它根据用户请求自动决定该调用哪个 Worker。
大家好,我是小撒。
上一篇讲到,多 Agent 不是为了堆概念,而是为了解决单 Agent 的几个现实问题:上下文太长、角色太杂、任务无法并行。
但只要你开始拆 Agent,很快就会遇到一个新问题:
多个 Agent 都有了,谁来决定谁先干?
上一个 Agent 的结果,怎么交给下一个 Agent?
用户一句自然语言请求,系统怎么知道该调用哪些能力?
这个角色就是 Orchestrator,也可以叫调度员。
调度员不是"更厉害的 Agent"
很多人第一次听 Orchestrator,会以为它是一个更强的 Agent。其实不是。
在这个小系统里,Orchestrator 自己不翻译、不摘要、不分析情绪。它只做一件事:看清楚任务怎么拆,然后把任务分给合适的 Worker。
这件事听起来普通,但它非常关键。因为一旦有了调度员,系统就从"几个函数堆在一起",变成了"有人负责流程"。
我把职责分成两类:
Orchestrator 负责:
- 理解用户要做什么
- 决定调用哪些 Worker
- 决定任务顺序
- 把上一步结果传给下一步
- 汇总最终输出
Worker 负责:
- 只完成自己那一小块任务
- 不关心全局流程
- 不关心前后还有谁
- 不替调度员做决策
这就是多 Agent 系统里最重要的分工:调度归调度,执行归执行。
先写三个 Worker
今天用一个文本处理系统做例子。我们先准备三个 Worker:翻译、摘要、情感分析。
python
# multi_agent/adv2_orchestrator.py
class TranslatorWorker:
"""只负责翻译,不管任务全局。"""
def run(self, text: str, target_lang: str = "英文") -> str:
messages = [
system("你是专业翻译,只输出翻译结果,不加任何说明。"),
user(f"将下面的内容翻译成{target_lang}:\n{text}"),
]
return chat(messages, temperature=0.2)
class SummarizerWorker:
"""只负责生成摘要。"""
def run(self, text: str, max_sentences: int = 3) -> str:
messages = [
system(f"你是摘要专家,用不超过{max_sentences}句话概括核心内容。"),
user(text),
]
return chat(messages, temperature=0.3)
class SentimentWorker:
"""只负责情感分析。"""
def run(self, text: str) -> str:
messages = [
system('分析文本情感,只输出 JSON:{"sentiment": "正面/负面/中性", "score": 0到1, "reason": "一句话原因"}'),
user(text),
]
return chat(messages, temperature=0.1)
这里有一个细节很重要:每个 Worker 都只知道自己的任务。Translator 不知道 Summarizer 的存在,Sentiment 也不知道自己是不是流程的最后一步。
这就是 Worker 的好处。以后你想加"关键词提取""标题生成""风险审查",只要新增 Worker,不需要把所有旧代码推翻重写。
Orchestrator 怎么做计划?
最简单的做法是写 if-else:
用户说"翻译",就调用 Translator;用户说"总结",就调用 Summarizer。
这个方法可靠,但不灵活。用户一旦说"先翻译成英文,再分析英文版的情感",规则就开始变复杂。
所以今天我让 LLM 来做计划:
python
def _plan_tasks(self, request: str) -> list[dict]:
"""让 LLM 理解用户意图,规划需要调用哪些 Worker。"""
messages = [
system("""你是任务规划器,根据用户请求输出需要执行的任务列表(JSON 格式)。
可用的 worker:
- translator: 翻译文本,参数 target_lang(目标语言)
- summarizer: 生成摘要,参数 max_sentences(句数,默认3)
- sentiment: 情感分析,无参数
输出格式(只输出 JSON 数组):
[{"worker": "名称", "params": {}, "use_prev": true/false}]
use_prev=true 表示这一步的输入是上一步的输出。
"""),
user(request),
]
raw = chat(messages, temperature=0.1)
start = raw.index("[")
end = raw.rindex("]") + 1
return json.loads(raw[start:end])
比如用户输入:
text
先翻译成英文,再分析英文版的情感
LLM 可能会规划出:
json
[
{"worker": "translator", "params": {"target_lang": "英文"}, "use_prev": false},
{"worker": "sentiment", "params": {}, "use_prev": true}
]
这里的 use_prev 很关键。它告诉 Orchestrator:第二步不要用原始文本,要用上一步翻译后的英文结果。
执行循环其实很简单
拿到计划之后,Orchestrator 按顺序跑就行:
python
def run(self, request: str, text: str) -> dict[str, str]:
print(f"[Orchestrator] 收到请求:{request}")
tasks = self._plan_tasks(request)
print(f"[Orchestrator] 任务计划:{[t['worker'] for t in tasks]}")
results: dict[str, str] = {}
prev_output = text
for task in tasks:
name = task["worker"]
params = task.get("params", {})
input_text = prev_output if task.get("use_prev") else text
print(f" → 调用 {name}({params})")
output = self._workers[name].run(input_text, **params)
results[name] = output
prev_output = output
return results
这段代码看起来不复杂,但已经有了一个多 Agent 流水线的雏形:计划、执行、传递、汇总。
跑起来看看
bash
cd multi_agent
python adv2_orchestrator.py
你会看到几种不同请求:
text
[Orchestrator] 收到请求:把这段话翻译成英文
[Orchestrator] 任务计划:['translator']
→ 调用 translator({'target_lang': '英文'})
[translator 结果]
Today a major update was released...
再看一个组合任务:
text
[Orchestrator] 收到请求:先翻译成英文,再分析英文版的情感
[Orchestrator] 任务计划:['translator', 'sentiment']
→ 调用 translator({'target_lang': '英文'})
→ 调用 sentiment({})
[translator 结果]
Today a major update was released...
[sentiment 结果]
{"sentiment": "正面", "score": 0.92, "reason": "内容积极,用户反馈正面"}
第二个例子里,Sentiment Worker 自动拿到了翻译后的英文,而不是原始中文。这就是 Orchestrator 的价值。
什么时候让 LLM 规划,什么时候直接写死?
这里有个坑:不要为了"智能"而什么都交给 LLM。
如果你的流程是固定的,比如永远是研究 → 写作 → 审核,那就直接写死:
python
research = researcher.run(topic)
draft = writer.run(research)
final = editor.run(draft)
这比让 LLM 每次临时规划更稳定。
我的原则是:
流程固定,用硬编码;流程动态,再让 LLM 规划。
比如"把用户的自然语言请求自动拆成翻译、摘要、情感分析"这种场景,适合 LLM 规划。因为用户的说法很多,你不可能提前写完所有规则。
但如果是固定生产线,就别让 LLM 临场发挥。工程系统里,能确定的部分尽量确定,只有不确定的部分才交给模型。
今天的代码结构
text
multi_agent/
├── llm.py # LLM 调用封装
├── adv1_context_demo.py # Day 1
├── adv2_orchestrator.py # Day 2
├── adv3_protocol.py # Day 3
├── adv4_parallel.py # Day 4
└── adv5_error_handling.py # Day 5
今天的重点不是代码有多复杂,而是把一个概念落到了可以运行的程序里:
Worker 负责干活,Orchestrator 负责调度。
下一篇写什么?
现在 Orchestrator 和 Worker 之间还是直接传参数:
python
worker.run(input_text, target_lang="英文")
简单是简单,但系统一大就会麻烦。因为每个 Worker 的参数不一样,Orchestrator 必须知道所有 Worker 的细节。
下一篇我们会给 Agent 之间的交接定一个固定格式:Task 和 TaskResult。有了这套协议,所有 Worker 的接口就能统一,出了 bug 也更容易查。