Multi-Agent 里谁来指挥?我用一个调度员,让多个 Agent 开始协作

这是《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 之间的交接定一个固定格式:TaskTaskResult。有了这套协议,所有 Worker 的接口就能统一,出了 bug 也更容易查。

相关推荐
范什么特西1 小时前
Spring boot细节
java·spring boot·后端
不喝水就会渴1 小时前
【共创季稿事节】HarmonyOS 7.0 时代的新基建 :DevEco CLI + Claude Code,鸿蒙 AI 开发的黄金搭档
人工智能·华为·harmonyos
星河耀银海1 小时前
大模型和搜索引擎到底有什么不一样
人工智能·搜索引擎
沪漂阿龙1 小时前
《LangChain》成本、限流、缓存、降级:AI 应用上线要考虑的问题
人工智能·langchain
一切皆是因缘际会1 小时前
RLHF奖励坍塌:大模型Reward漂移机理
人工智能·数学建模·ai
阿庆_AI研发工程师1 小时前
从 OpenAI Codex 源码看生产级 AI Agent Runtime 的工程模式
人工智能
武子康1 小时前
调查研究-177 Agent / Harness 工具链研究:从会调用工具的 LLM,到可观测、可验证、可交付的智能体系统
人工智能
集芯微电科技有限公司1 小时前
四通道2A输出集成功率电感降压模块专为紧凑型方案设计
人工智能·单片机·嵌入式硬件·生成对抗网络·计算机外设
朱大喜2 小时前
NumPy 性能优化:内存布局、向量化与原地操作的实战经验
人工智能