从零手写多 Agent 编排框架:200 行 Python 跑通 Orchestrator-Worker
上一篇《从单兵到军团》我把多智能体的概念、三种编排模式、四大治理难题讲了一遍。但光看理论,你心里一定痒:这玩意到底怎么落地? 市面上有 LangGraph、CrewAI 一大堆框架,可如果直接套框架,你永远看不清它内核里"任务怎么拆、消息怎么传、结果怎么汇总"这三件事。所以这篇我不碰任何框架,用 200 行纯 Python 从零手写一个 Orchestrator-Worker 编排框架。写完你会明白:多 Agent 的骨架其实就那么几根梁,框架只是给它刷了层漆。
一、先想清楚:一个多 Agent 框架最少要有几块?
不急着写代码,先回答一个更底层的问题------让多个 Agent 协作,本质需要解决哪几件事?
我把它拆成四块,缺一不可:
- 统一的语言 :Agent 之间得说同一种"话",否则一个发 JSON 一个发纯文本,系统立刻乱套。这就是消息契约(Message Schema)。
- 会干活的个体:职责单一的 Worker,给它一个明确任务,它返回一个明确结果,不多管闲事。
- 会调度的大脑:Orchestrator,负责理解总目标、拆成子任务、分发给合适的 Worker、再把结果拼起来。
- 能聊天的底座:一个 LLM 调用封装。多 Agent 本质是"多次 LLM 调用 + 一套编排逻辑",LLM 是燃料,编排是发动机。

上面这张图就是这篇要搭的全部。别被"框架"俩字吓到,它翻译成人话就一句:Orchestrator 拆活 → Worker 干活 → 结果沿消息契约回流 → Orchestrator 拼装。下面一块块把它垒起来。
二、第 0 块:一个模型无关的 LLM 封装
多 Agent 框架应该跟模型供应商解耦 。我用 openai 这个兼容库,把 base_url 和 api_key 做成参数,这样一会儿跑 Grok、一会儿跑 DeepSeek、一会儿跑 OpenAI,只改一个配置。
python
# llm.py ------ 模型无关的调用封装
import os
from openai import OpenAI
class LLMClient:
"""OpenAI 兼容的 LLM 封装,base_url 可切换任意供应商"""
def __init__(self, base_url, api_key, model):
self.model = model
self.client = OpenAI(base_url=base_url, api_key=api_key)
def chat(self, system: str, user: str, temperature: float = 0.2) -> str:
resp = self.client.chat.completions.create(
model=self.model,
temperature=temperature,
messages=[
{"role": "system", "content": system},
{"role": "user", "content": user},
],
)
return resp.choices[0].message.content
@staticmethod
def default():
# 这里以 xAI Grok 为例;换成 OpenAI/DeepSeek 只改这两行
return LLMClient(
base_url="https://api.x.ai/v1",
api_key=os.getenv("XAI_API_KEY", "your-key"),
model="grok-4.5",
)
要点就一个:chat 函数只认 system + user 两段,别的都不管。这样所有 Agent 共用同一个口子,你后面想接流式、想接缓存,都在这一层改,Agent 代码一行不用动。
三、第 1 块:消息契约,Agent 之间的"通用语"
这是整个框架的基石,也是 90% 的"从零写多 Agent"教程会漏掉的部分。没它,你的系统越复杂越像一锅粥。
python
# message.py ------ 统一消息契约
import json
from dataclasses import dataclass, field, asdict
from typing import Any, Optional
@dataclass
class Message:
"""Agent 间传递的唯一消息结构"""
role: str # "task" 任务 | "result" 结果 | "question" 追问
from_agent: str # 发送方 Agent 名
to_agent: str # 接收方;"orchestrator" 表示回主控
task_id: str # 归属的任务 id,追踪靠它
payload: dict # 结构化业务内容
meta: dict = field(default_factory=dict) # 预留:优先级、重试次数等
def to_json(self) -> str:
return json.dumps(asdict(self), ensure_ascii=False)
@staticmethod
def task(from_agent: str, to_agent: str, task_id: str, instruction: str, **extra):
return Message("task", from_agent, to_agent, task_id, {"instruction": instruction, **extra})
@staticmethod
def result(from_agent: str, task_id: str, output: Any):
return Message("result", from_agent, "orchestrator", task_id, {"output": output})
为什么非要用一个 Message 包起来?三个理由:
- 可扩展 :以后要加优先级、加超时、加加密,往
meta里塞就行,不动结构。 - 可追踪 :每条消息都带
task_id和from/to,出了岔子你能顺着它还原整条链路(下一篇专门讲这个)。 - 可序列化 :
to_json一下,消息就能落库、能跨进程、能进消息队列。单机多 Agent 和多机分布式多 Agent 之间,就差一个把to_json结果丢进 MQ 的动作。

四、第 2 块:Worker,职责单一的"干活的"
Worker 的设计哲学就一条:让它笨一点,只干一件事。不要给 Worker 塞一个万能 prompt,那又退回单 Agent 的老路了。
python
# worker.py ------ 职责单一的 Worker
from llm import LLMClient
from message import Message
class Worker:
"""一个职责单一的 Agent,只负责执行分配到的任务"""
def __init__(self, name: str, role_desc: str, llm: LLMClient):
self.name = name
self.llm = llm
self.system_prompt = (
f"你是「{name}」,职责:{role_desc}。\n"
"你会收到一条 JSON 格式的任务消息,请只完成其中的 instruction 描述的任务。\n"
"只输出最终结果本身,不要寒暄、不要解释过程、不要输出 JSON 包装。"
)
def run(self, msg: Message) -> Message:
"""接收任务消息,返回结果消息"""
reply = self.llm.chat(self.system_prompt, msg.to_json())
return Message.result(self.name, msg.task_id, reply)
看几个细节,它们不是随便写的:
- system prompt 里"只输出结果本身":这一步直接决定你后面汇总时干不干净。如果 Worker 输出一堆"好的,我来帮你......"的开场白,Orchestrator 拼结果时会抓狂。
run的入参和出参都是Message:Worker 不知道也不关心任务从哪来、结果去哪。它是个纯函数式的黑盒,这就是"职责单一"在代码层面的体现。
五、第 3 块:Orchestrator,会调度的大脑
到这里,主控登场。它的职责分两半:上半场拆任务,下半场拼结果。拆任务交给 LLM 自己判断,拼结果也交给 LLM,主控本身只做"分发 + 汇总"的编排动作。
python
# orchestrator.py ------ 拆解 + 分发 + 汇总
import json
from llm import LLMClient
from message import Message
from worker import Worker
class Orchestrator:
def __init__(self, llm: LLMClient, workers: dict[str, Worker]):
self.llm = llm
self.workers = workers
# 把 worker 清单喂给模型,让它在拆任务时就知道能派给谁
self._roster = "\n".join(f"- {n}: {w.system_prompt.split('职责:')[1].split('。')[0]}"
for n, w in workers.items())
def _plan(self, goal: str) -> list[dict]:
"""让 LLM 把总目标拆成 (worker, instruction) 列表"""
sys = (
"你是任务规划器。把用户目标拆成若干可并行的子任务。\n"
f"可用 Worker 清单:\n{self._roster}\n"
"只输出 JSON 数组,每个元素形如 "
'{"worker": "名字", "instruction": "给该 Worker 的指令"}。'
)
raw = self.llm.chat(sys, f"用户目标:{goal}")
# 兜底:LLM 偶尔会在 JSON 外面包一段话,这里做个容错提取
return json.loads(self._extract_json(raw))
def _extract_json(self, raw: str) -> str:
start, end = raw.find("["), raw.rfind("]")
return raw[start:end + 1] if start != -1 and end != -1 else raw
def run(self, goal: str) -> str:
plan = self._plan(goal)
# 逐个分发(先串行,下一篇再讲并行和状态管理)
results = []
for i, task in enumerate(plan):
worker = self.workers[task["worker"]]
task_id = f"{goal[:8]}-{i}" # 简化版 task_id
msg = Message.task("orchestrator", worker.name, task_id, task["instruction"])
result = worker.run(msg)
results.append({"worker": task["worker"], "output": result.payload["output"]})
# 汇总:让 LLM 把各 Worker 的结果拼成最终交付物
return self._synthesize(goal, results)
def _synthesize(self, goal: str, results: list[dict]) -> str:
sys = "你是总编。把多个 Worker 的产出整合成一份面向用户的最终答复,条理清晰。"
return self.llm.chat(sys, f"目标:{goal}\n各 Worker 产出:\n{json.dumps(results, ensure_ascii=False)}")
_plan 里那句 self._roster 特别关键------拆任务不是 Orchestrator 拍脑袋拆,而是看着"手头有哪些人"来拆。你团队里只有"研究员"和"写手",它就不会拆出一个"写代码"的任务。这正是多 Agent 比单 Agent 稳的地方:能力边界是显式声明出来的,而不是藏在 prompt 里。
六、把它们拼起来跑一个真实任务
四块集齐,写个 main.py 验证。用最经典的"研究员 + 写手"双 Worker 团队,让它产出一篇技术短文。
python
# main.py ------ 跑通一个「研究员 + 写手」双 Agent 团队
from llm import LLMClient
from worker import Worker
from orchestrator import Orchestrator
llm = LLMClient.default()
# 组队:两个职责单一的 Worker
researcher = Worker("研究员", "负责搜集事实、数据和来源,产出要点清单", llm)
writer = Worker("写手", "负责把要点写成通俗易懂的中文短文", llm)
# 主控接管
boss = Orchestrator(llm, {"研究员": researcher, "写手": writer})
goal = "写一篇 300 字短文,介绍什么是 MCP(Model Context Protocol),面向零基础读者"
print(boss.run(goal))
跑起来你会看到这样一段协作(我把关键流转标出来了):

它内部的完整链路是 :main 把目标丢给 Orchestrator → _plan 让 LLM 拆出「研究员:查 MCP 是什么」和「写手:写 300 字短文」两个子任务 → 逐个派给对应 Worker → 研究员返回要点、写手基于要点成文 → _synthesize 把两份产出拼成最终答案。全程你只写了一句 boss.run(goal)。
这就是多 Agent 和"你在 for 循环里调两次 LLM"的本质区别:任务拆解、能力路由、结果整合这三件事,是被显式建模、可观测、可复用的,而不是散落在你的一次性脚本里。
七、这份"最小实现"还缺什么?
坦白讲,现在这套只能算"玩具"。它有明显的窟窿,我故意留着,下一篇一个个补上:
- 没有并行:子任务一个接一个跑,慢。研究员和写手明明能并行。
- 没有状态 :
task_id是goal[:8]这种偷懒写法,撞了都不知道;也没有任务看板,哪个完成了、哪个卡住了,黑盒。 - 没有容错 :某个 Worker 超时或返回垃圾,整个
run直接炸,没有重试、没有降级。 - 没有成本意识:拆 5 个子任务就调 5 次 LLM,还都是同一个模型,烧钱没有上限。

这四件事,就是**"能跑的 Demo"和"能上线的系统"之间的全部距离**。别急着跳进 LangGraph------它内部也就是把这些事帮你做掉了,你先把窟窿看清,下一篇我带你用代码一个个堵上:并行分发 + 任务看板状态机 + 超时重试降级 + 模型分级预算熔断。
八、小结
这一篇我们没碰任何框架,用 200 行 Python 垒出了多 Agent 的骨架:LLM 封装 → 消息契约 → Worker → Orchestrator。核心感悟就一句------
多 Agent 框架拆穿了,就是「一次会调 LLM 的编排逻辑」。真正的难点从来不是模型不够聪明,而是你怎么定义消息契约、怎么显式建模任务拆解、怎么让每个环节可观测。这几件事框架帮不了你,只能你自己想清楚。
下一篇(07)我接着这套代码往下写,给它加上生产级三件套:状态机、分布式追踪、错误降级与成本控制,让这个玩具真正能扛住生产环境的毒打。先把这篇的代码在本地跑通,下一篇我们继续。
作者 :夏文强 | OpenHarmony 贡献者,专注 AI Agent 工程化落地
专栏 :AI Agent 实战笔记
觉得有用就点个赞 👍,有疑问欢迎评论区交流~