多 Agent 系统生产化:状态机、分布式追踪、错误降级、成本控制四件套
上一篇我从零手写了一个 200 行的 Orchestrator-Worker 框架,也老实交代了它有四个窟窿:不能并行、没有状态、没有容错、不控成本 。有人会问:既然 LangGraph 都把这些做掉了,我干嘛还自己造?答案是------你只有亲手把窟窿堵一遍,才真正理解"多 Agent 为什么难",以后不管用哪个框架,一眼就能看出它哪里会坑你。这篇就接着上篇的代码,把这四个窟窿一个个堵上。补完的这套,已经算得上一个能扛住生产环境毒打的迷你编排层了。
一、先给"任务"立个户口:状态机
上篇最偷懒的地方,是 task_id = goal[:8] 这种写法,而且任务跑没跑完、结果在哪,你完全不知道。生产化第一步,是让每个任务都有户口、有状态。
python
# state.py ------ 任务状态机 + 看板
from dataclasses import dataclass, field
from typing import Any, Optional
class TaskStatus:
PENDING = "pending" # 待分配
RUNNING = "running" # 执行中
DONE = "done" # 成功
FAILED = "failed" # 失败
DEGRADED = "degraded" # 降级完成(部分结果)
@dataclass
class Task:
id: str
worker: str
instruction: str
status: str = TaskStatus.PENDING
result: Any = None
error: str = ""
trace_id: str = ""
retries: int = 0
这就是最朴素的状态机:pending → running → done/failed/degraded。别小看这几个字符串------有了它,你的系统从"黑盒"变成了"看板"。任何一个时刻,你都能回答"这个任务到哪一步了、结果是什么、卡了多久"。

二、给每条消息打上"链条":分布式追踪
状态机解决了"单个任务到哪了",但多 Agent 的问题常常是跨任务的 :最终结果错了,你得知道是研究员查错、还是写手写错、还是主控拼错。这就要分布式追踪 ------给每次"用户目标"发一个 trace_id,这条链上所有消息、所有任务都带同一个 id。
改造上篇的 Message,加一行:
python
import uuid
# 一次 run 开一条 trace,全程消息共享同一个 trace_id
trace_id = uuid.uuid4().hex
然后 Task.trace_id = trace_id,Message.meta["trace_id"] = trace_id。出问题时,你按 trace_id 一捞,整条链路的来龙去脉全出来了。这也是为什么我上篇坚持用统一 Message 契约------追踪能力是"长"在消息结构上的,不是事后硬加的。

三、让 Worker 跑起来:并行分发
状态和追踪就位,先解决最影响体验的"慢"。上篇是串行 for 循环,现在用线程池并行。多 Agent 的子任务天然可并行,收益立竿见影。
python
# production.py ------ 并行 + 状态 + 降级 + 成本 的生产级编排器(核心)
import uuid
from concurrent.futures import ThreadPoolExecutor, as_completed, TimeoutError
from llm import LLMClient
from message import Message
from worker import Worker
from state import Task, TaskStatus
class ProductionOrchestrator:
def __init__(self, llm: LLMClient, workers: dict[str, Worker],
backup: dict[str, str] | None = None,
max_workers: int = 4, budget_calls: int = 20):
self.llm = llm
self.workers = workers
self.backup = backup or {} # worker -> 备用 worker 名
self.max_workers = max_workers
self.budget_calls = budget_calls # 熔断预算
self._calls = 0
def _charge(self, n: int = 1):
"""计数熔断:超出预算直接抛错,防止烧钱失控"""
self._calls += n
if self._calls > self.budget_calls:
raise RuntimeError(f"超出 LLM 调用预算({self.budget_calls}),已熔断")
def run(self, goal: str) -> str:
trace_id = uuid.uuid4().hex
plan = self._plan(goal) # 拆任务
tasks = [Task(id=f"{trace_id[:8]}-{i}", worker=t["worker"],
instruction=t["instruction"], trace_id=trace_id)
for i, t in enumerate(plan)]
# 并行分发,带状态回写
with ThreadPoolExecutor(max_workers=self.max_workers) as pool:
futures = {pool.submit(self._execute, t): t for t in tasks}
for fut in as_completed(futures):
t = futures[fut]
try:
t.result = fut.result(timeout=60) # 超时兜底
t.status = TaskStatus.DONE
except Exception as e:
t.error = str(e)
t.status = TaskStatus.FAILED
# 失败任务尝试降级
for t in tasks:
if t.status == TaskStatus.FAILED:
t.status, t.result = self._degrade(t)
return self._synthesize(goal, tasks)
as_completed 的好处是谁先干完先回来,不用等最慢的那个 。fut.result(timeout=60) 给了每个任务一个 60 秒的硬上限,超时的任务不会拖死整条链路。
四、天塌了怎么办:错误降级
并行只是"跑得快",生产环境真正要命的是某个 Worker 挂了,整个任务不能跟着陪葬。降级策略我做成三级,逐级兜底:
python
def _degrade(self, task: Task):
# 第一级:重试一次(可能是网络抖动)
if task.retries < 1:
task.retries += 1
try:
out = self._execute(task)
return TaskStatus.DONE, out
except Exception as e:
task.error = str(e)
# 第二级:切换备用 Worker(A 挂了切 B)
backup_name = self.backup.get(task.worker)
if backup_name and backup_name in self.workers:
try:
out = self._execute(task, worker_name=backup_name)
return TaskStatus.DEGRADED, out # 标记"降级完成"
except Exception as e:
task.error = str(e)
# 第三级:主控自己兜底(orchestrator 用强模型硬顶)
fallback = self.llm.chat(
"你是兜底执行者,尽力完成这个任务。",
f"任务:{task.instruction}",
)
return TaskStatus.DEGRADED, fallback

核心原则一句话:局部的失败不能拖垮全局。宁可给用户一个"降级但可用"的结果,也不要整条链路因为一个 Worker 超时而全盘报错。这也是多 Agent 相比单 Agent 的一个隐藏优势------单 Agent 挂了就是全挂,多 Agent 挂一个还能换一个顶。
五、钱不能白烧:模型分级 + 预算熔断
最后是成本。多 Agent = 多次 LLM 调用,不设防的话,一个需求拆 10 个子任务、每个还重试两轮,token 是单 Agent 的十几倍。两个手段:
手段一:模型分级。 不是所有环节都配用旗舰模型。编排、汇总、兜底用强模型,普通子任务用便宜模型 。上篇我的 Worker 和 Orchestrator 共用一个 llm,这里拆开------Worker 换成更便宜的模型,主控保留旗舰。
python
strong_llm = LLMClient(..., model="grok-4.5") # 编排/汇总/兜底
cheap_llm = LLMClient(..., model="grok-4.1") # 普通子任务执行
boss = ProductionOrchestrator(
llm=strong_llm,
workers={"研究员": Worker("研究员", ..., cheap_llm),
"写手": Worker("写手", ..., cheap_llm)},
budget_calls=20,
)
一个工程直觉:90% 的 token 都花在"执行"上,而不是"编排"上。所以省钱的大头是把执行层降配,而不是在编排层抠那两三次调用。
手段二:预算熔断。 上面的 _charge / budget_calls 就是------给每次 run 设一个调用次数上限,超了直接抛错停机。宁可任务失败,也不要失控烧钱。生产上这个预算还会细化到 token 级,但调用次数上限是最容易落地、最有效的第一道闸。

六、整套代码长这样
把上面几段拼起来(加上 _plan、_execute、_synthesize 三个方法和上篇复用的 llm.py/message.py/worker.py),就是一套能直接跑的生产级编排层。完整文件我放在文末可运行清单里,这里只补最关键的两个方法:
python
def _plan(self, goal: str) -> list[dict]:
self._charge()
raw = self.llm.chat(
f"你是任务规划器,可用 Worker:{self._roster()}。"
"只输出 JSON 数组:[{\"worker\":..., \"instruction\":...}]",
goal)
import json
return json.loads(self._extract_json(raw))
def _execute(self, task: Task, worker_name: str | None = None):
self._charge()
name = worker_name or task.worker
msg = Message.task("orchestrator", name, task.id, task.instruction)
msg.meta["trace_id"] = task.trace_id
return self.workers[name].run(msg).payload["output"]
def _synthesize(self, goal: str, tasks: list[Task]) -> str:
self._charge()
summary = [{"worker": t.worker, "status": t.status,
"output": t.result, "error": t.error} for t in tasks]
return self.llm.chat(
"你是总编,整合各 Worker 产出;标注出降级/失败的任务。",
f"目标:{goal}\n各任务结果:{summary}")
注意 _synthesize 里我让主控把"降级/失败的任务"也标出来------生产系统里,诚实报告"哪个环节降级了"比憋出一个看似完美的答案重要得多。
七、小结
到这儿,上篇的四个窟窿全部堵上了:
| 窟窿 | 解法 | 关键动作 |
|---|---|---|
| 不能并行 | ThreadPoolExecutor |
子任务并发,谁先完谁先回 |
| 没有状态 | Task 状态机 |
pending→running→done/failed/degraded |
| 没有容错 | 三级降级 | 重试 → 切备用 → 主控兜底 |
| 不控成本 | 分级 + 熔断 | 执行层降配 + 调用预算上限 |
回头看,这四件事没有一件是"模型能力"问题,全是"工程治理"问题 。这也是我对多 Agent 的核心判断:2026 年的分水岭,早就从"模型聪不聪明"移到了"你会不会做分布式系统的治理"。
下一篇(08)是这个系列的收尾,也是最反直觉的一篇:我会把中心化编排、任务交接(Swarm)、竞争择优(Tournament)三种模式各写一个最小实现,跑同一个任务对比 ,再讲一件很多人逃避的事------多 Agent 系统到底怎么评测。不是因为模型更强所以更好,而是因为编排对了所以更好,下一篇我们用数据说话。
作者 :夏文强 | OpenHarmony 贡献者,专注 AI Agent 工程化落地
专栏 :AI Agent 实战笔记
觉得有用就点个赞 👍,有疑问欢迎评论区交流~