多智能体编排与工作流引擎

多智能体不是把多个 Agent 拼起来就完事,编排结构决定了系统的可控性、成本与可调试性。本文对比单体 Agent 与多 Agent 的取舍,拆解 ReAct、Plan-Execute、Reflexion 三种范式,深入 LangGraph 的状态、检查点与图式编排,给出子 Agent 委派、共享记忆、失败重试、人机协同与成本控制的工程方案,附可运行的状态图代码。

当团队把单 Agent 跑通之后,下一个冲动通常是「上多 Agent」。这个冲动需要被克制。多 Agent 不是能力的线性叠加,而是复杂度的指数上升:调试从「看一条链路」变成「看多个 Agent 之间的对话」,成本从「一次调用」变成「N 个 Agent 各自循环」,失败模式从「模型答错」变成「两个 Agent 互相推诿、或者无限对话」。绝大多数业务用单体 Agent 加工具就够了,多 Agent 只在明确需要「角色分工」或「并行探索」时才划算。

本文与 Agent 服务的可观测性与链路追踪 划清边界:那篇讲的是 Agent 上线后怎么监控、怎么打 span、怎么告警;本文讲的是编排结构本身——状态怎么定义、控制流怎么走、子 Agent 怎么委派、失败怎么恢复。前者是「看得见」,后者是「搭得对」。

编排的本质是把不确定的模型决策,放进一个确定的控制结构里。ReAct 循环是最简单的结构,图式编排(LangGraph 类)是最灵活的结构,多 Agent 是最重的结构。选择的依据是任务的确定性程度:越确定的任务,越应该用代码而不是模型来控制流程。Agent 的基础构件(模型、工具、记忆、循环)可先参考 Agent 架构 建立整体认知。

目录

  1. 单体 Agent 与多 Agent 的取舍
  2. 编排范式:ReAct、Plan-Execute 与 Reflexion
  3. 图式编排与状态机
  4. 检查点、恢复与持久化
  5. 工具路由与子 Agent 委派
  6. 多 Agent 通信与共享记忆
  7. 失败重试、超时与死循环防护
  8. 人机协同与审批节点
  9. 编排的可观测与成本控制
  10. 生产级编排的落地清单
  11. 权衡取舍
  12. 常见坑清单
  13. 小结

1. 单体 Agent 与多 Agent 的取舍

先明确两者的定义。单体 Agent 是一个模型加一组工具,在一个循环里自主决定调用哪个工具、何时结束。多 Agent 是多个各有角色与工具集的模型,通过某种协议协作完成一个任务。

多 Agent 的三种典型收益:

  • 角色分工:不同角色有不同提示与工具集,比如「检索员」只能读、「编辑」只能写、「审核员」只能判定。分工让每个 Agent 的提示更聚焦,工具权限更小。
  • 并行探索:多个 Agent 同时从不同角度解题,最后汇总。适合搜索、头脑风暴类任务。
  • 上下文隔离:不同 Agent 维护各自的上下文窗口,避免单个超长上下文稀释注意力。

代价同样明确:

维度单体 Agent多 Agent
延迟一次循环多轮串行或并行,通常 2 到 5 倍
token 成本1×2 到 10×(Agent 间通信也消耗 token)
调试难度一条链路多条链路加 Agent 间对话
失败模式答错、循环上述全部,加推诿、死锁、信息丢失
可控性高低(涌现行为难预测)

判断标准很简单:如果单 Agent 加一个好提示能解决,就不要上多 Agent。真正需要多 Agent 的信号是「任务天然有多个阶段且各阶段需要不同权限或上下文」,而不是「任务看起来复杂」。

2. 编排范式:ReAct、Plan-Execute 与 Reflexion

三种范式是递进的抽象层次。

ReAct

ReAct(Reason + Act)是最基础的循环:模型输出「思考 → 行动 → 观察」,把观察结果塞回上下文,继续下一轮,直到输出最终答案。

循环:
  Thought: 我需要查一下今天的汇率
  Action:  search("USD to CNY rate today")
  Observation: 7.12
  Thought: 有了汇率,可以计算了
  Action:  calculate("1000 * 7.12")
  Observation: 7120
  Thought: 可以给出最终答案
  Final Answer: 1000 美元约等于 7120 人民币

ReAct 的优点是简单、通用、对工具数量不敏感;缺点是每步都要一次模型调用,延迟高,且容易陷入「反复调用同一个工具」的循环。

Plan-Execute

Plan-Execute 把「规划」与「执行」分离:先让模型产出一个完整计划(步骤列表),再逐步执行,执行中可根据结果修订计划。

Plan: [
  "查询公司 2025 年财报",
  "提取营收与净利润",
  "计算同比增长率",
  "生成对比图表数据",
]
Execute: 逐步执行,每步结果可触发 Replan

优势是 token 效率更高(规划只需一次调用),且计划本身可审查、可人工干预;劣势是计划质量依赖模型对任务的理解,任务中途出现意外时需要 Replan 机制兜底。

Reflexion

Reflexion 在循环里加入「自我反思」:任务失败后,让模型分析失败原因并生成改进策略,存进记忆,下次尝试时带上。

Attempt 1: 失败 → Reflection: "我漏了检查参数单位,下次应先确认单位"
Attempt 2: 带上反思重试 → 成功

三种范式的适用:

范式适用任务延迟成本实现复杂度
ReAct工具调用型、步骤少低低低
Plan-Execute多步骤、步骤可预先规划中中中
Reflexion可重试、有明确成败判据高高中高

实践中最常用的是「Plan-Execute 为主 + ReAct 执行单步 + 关键步骤 Reflexion」,这个组合能兼顾效率与鲁棒性。不同框架对这三种范式的封装差异,可以参考 Agent 框架 的横向对比。

3. 图式编排与状态机

当控制流变得复杂(分支、循环、并行、人工节点),把逻辑写在提示词里就失控了。图式编排把 Agent 的执行建模成一张有向图:节点是计算单元(模型调用、工具调用、判断),边是控制流。这一思路是 工作流编排 的工程化落地,也是本节与后续小节的基础。

以 LangGraph 为例,一张典型的客服 Agent 图:

from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import MemorySaver

class AgentState(TypedDict):
    messages: Annotated[list, add_messages]   # 消息列表,自动追加
    intent: str                               # 意图分类结果
    attempts: int                             # 重试计数
    needs_human: bool                         # 是否需要人工

def classify(state: AgentState) -> dict:
    """意图分类节点"""
    intent = classify_intent(state["messages"][-1].content)
    return {"intent": intent}

def route(state: AgentState) -> str:
    """条件边:按意图路由到不同处理节点"""
    if state["intent"] == "refund":
        return "refund_handler"
    if state["intent"] == "tech":
        return "tech_handler"
    return "general_handler"

def guard(state: AgentState) -> dict:
    """兜底节点:重试超限则转人工"""
    if state["attempts"] >= 3:
        return {"needs_human": True}
    return {"attempts": state["attempts"] + 1}

builder = StateGraph(AgentState)
builder.add_node("classify", classify)
builder.add_node("refund_handler", refund_handler)
builder.add_node("tech_handler", tech_handler)
builder.add_node("general_handler", general_handler)
builder.add_node("guard", guard)

builder.add_edge(START, "classify")
builder.add_conditional_edges("classify", route,
    {"refund_handler": "refund_handler",
     "tech_handler": "tech_handler",
     "general_handler": "general_handler"})
builder.add_edge("refund_handler", "guard")
builder.add_edge("tech_handler", "guard")
builder.add_edge("general_handler", "guard")
builder.add_conditional_edges("guard",
    lambda s: "human" if s["needs_human"] else END,
    {"human": END})

graph = builder.compile(checkpointer=MemorySaver())

图式编排的三个价值:

  1. 控制流显式化。分支、循环、并行都用边表达,代码可读、可测试、可画出来。
  2. 状态是单一事实来源。所有节点读写同一个 AgentState,避免散落在各处的全局变量。
  3. 天然支持恢复。图是有状态的,配合 checkpointer 可以在任意节点暂停与恢复。

4. 检查点、恢复与持久化

长任务(可能几分钟到几小时)必须支持「暂停后恢复」,否则一次网络抖动就要从头再来。检查点(checkpoint)把每一步的状态快照持久化。

from langgraph.checkpoint.postgres import PostgresSaver

with PostgresSaver.from_conn_string(          # 生产用 Postgres 持久化,不用内存
    "postgresql://user:pass@db:5432/agent"
) as checkpointer:
    checkpointer.setup()                    # 建表,首次运行
    graph = builder.compile(checkpointer=checkpointer)

config = {"configurable": {"thread_id": "session-8821"}}
graph.invoke({"messages": [("user", "帮我退掉订单 A123")]}, config)  # 首次执行

state = graph.get_state(config)
if state.next:                             # 服务重启后,还有未完成的节点
    graph.invoke(None, config)             # 传入 None 表示从检查点续跑

检查点设计的关键点:

要素要求说明
thread_id每次会话唯一恢复的锚点,必须稳定
存储后端Postgres / Redis内存版仅用于开发
快照粒度每个节点后太粗丢进度,太细写放大
保留策略按天分区 + TTL避免快照无限膨胀
序列化JSON / msgpack注意自定义对象的可序列化

一个常被忽略的成本:检查点会保存完整的消息列表。一个 20 步的会话,消息列表可能几万 token,每步都存一份,存储与序列化开销可观。优化手段是只存「必要状态」(意图、进度、关键变量),把完整消息列表单独存、按需加载。

5. 工具路由与子 Agent 委派

当工具数量超过十几个,把所有工具塞进一个提示会稀释模型注意力、增加误调用。解决方式是分层:主 Agent 只面对少数「子 Agent」或「工具组」,由子 Agent 再面对具体工具。

委派模式

from langgraph.prebuilt import create_react_agent

research_agent = create_react_agent(          # 子 Agent:聚焦的提示与工具集
    model, tools=[web_search, read_pdf],
    prompt="你是研究员,负责查找与整理资料,不写最终答案。",
)
writer_agent = create_react_agent(
    model, tools=[write_file],
    prompt="你是撰稿人,只负责把资料组织成文,不检索。",
)


def supervisor(state: AgentState) -> dict:
    """主管 Agent:只做委派决策,不直接调工具"""
    decision = model.chat(
        system="根据任务决定下一步交给 research 还是 writer,"
               "若已完成则回答 FINISH。",
        user=state["messages"][-1].content,
    )
    return {"next": decision.content.strip()}

def delegate(state: AgentState) -> dict:
    target = research_agent if state["next"] == "research" else writer_agent
    result = target.invoke({"messages": state["messages"]})
    return {"messages": result["messages"]}

主管模式(supervisor)的价值在于:委派决策与执行分离。主管只看任务摘要,不需要持有所有工具的细节,上下文更短、决策更聚焦。

工具路由

比委派更轻的做法是「工具分组 + 按需加载」:根据意图先选一组工具,再把这些工具的 schema 传给模型。

路由方式机制延迟适用
全量暴露所有工具都传给模型低工具少于 10 个
意图路由先分类意图,再选工具组中工具 10 到 50 个
检索路由用查询检索最相关的工具中工具超过 50 个
子 Agent 委派每类任务一个子 Agent高需要角色隔离

工具路由的收益是可量化的:把 40 个工具的 schema(约 6000 token)路由到平均 8 个工具(约 1200 token),每次调用省下约 4800 输入 token。在每天百万次调用的规模下,这是可观的成本节省。

6. 多 Agent 通信与共享记忆

多 Agent 协作的核心难题是「信息怎么传」。三种模式(更完整的多 Agent 协同模式对比见 多智能体协同 ):

模式一:共享黑板

所有 Agent 读写同一个状态对象(如 LangGraph 的 AgentState),通过 add_messages 这类 reducer 追加信息。

  • 优点:实现简单,信息全局可见。
  • 缺点:状态膨胀,后加入的 Agent 要读全部历史。

模式二:消息传递

Agent 之间显式发消息,每个 Agent 维护自己的上下文。

  • 优点:上下文隔离,各自聚焦。
  • 缺点:需要协议设计,消息可能丢失或被误解。

模式三:共享记忆

把长期事实放进共享的记忆层(向量库或结构化存储),Agent 按需检索。

  • 优点:信息可持久、可跨会话。
  • 缺点:引入检索延迟与一致性问题。
模式上下文成本一致性实现复杂度适用
共享黑板高(全量可见)强低少量 Agent、短会话
消息传递低(隔离)中中角色清晰的流水线
共享记忆中(按需检索)弱(最终一致)高长会话、跨会话

实践中最常见的是「黑板做短期状态 + 共享记忆做长期事实」的组合。共享记忆的设计(写入时机、去重、遗忘策略)本身是一块独立课题,Agent 记忆的持久化方案可以参考专门的方法论。

一个必须强调的坑:Agent 之间的通信也会消耗 token,且往往是隐性的。三个 Agent 各传递一轮完整上下文,token 消耗是单 Agent 的三倍以上。因此多 Agent 的通信内容应该尽量是「结论」而非「原始材料」。

7. 失败重试、超时与死循环防护

Agent 的失败模式比普通服务多得多,必须逐类设防。

失败模式表现防护手段
工具超时单次调用挂起每工具超时 + 熔断
格式错误模型输出无法解析schema 校验 + 有限重试
死循环反复调用同一工具步数上限 + 重复检测
上下文溢出消息列表超长历史裁剪 + 摘要
委派推诿Agent 间互相转交委派深度上限
成本失控token 消耗爆炸每会话预算 + 硬熔断

死循环检测

最实用的检测是「状态指纹」:把每步的(工具名,参数哈希)记下来,如果连续几步指纹相同,判定为循环。

import hashlib, json

class LoopGuard:
    def __init__(self, max_repeats: int = 2, max_steps: int = 15):
        self.seen: list[str] = []
        self.max_repeats = max_repeats
        self.max_steps = max_steps

    def _fingerprint(self, tool: str, args: dict) -> str:
        payload = json.dumps({"t": tool, "a": args}, sort_keys=True)
        return hashlib.sha256(payload.encode()).hexdigest()[:16]

    def check(self, step: int, tool: str, args: dict) -> tuple[bool, str]:
        if step >= self.max_steps:
            return True, f"步数超限 {self.max_steps}"
        fp = self._fingerprint(tool, args)
        if self.seen.count(fp) >= self.max_repeats:
            return True, f"检测到重复调用 {tool}"
        self.seen.append(fp)
        return False, "ok"

max_repeats=2 意味着同一个工具加同一组参数最多调用两次,第三次就中断。这条规则能拦住绝大多数病态循环,且不会误伤合理的重复调用(比如对不同的输入调用同一工具,参数哈希不同)。

超时的分层

超时不能只有一个全局值,要分层设置:

层级超时超时后行为
单次模型调用30 - 60 s重试一次,再失败则降级
单次工具调用5 - 15 s返回错误给模型,让它换策略
单步(模型 + 工具)90 s中断该步,回退到上一个检查点
整会话5 - 10 min强制结束,转人工

分层超时的价值是「局部失败不升级为整体失败」:一个工具超时不应该让整个会话失败,而应该作为一次观察反馈给模型,让它决定下一步。

8. 人机协同与审批节点

有些决策不该由 Agent 自主完成:退款金额超过阈值、删除数据、对外发送内容。这些节点必须插入人工审批。

from langgraph.types import interrupt

def refund_node(state: AgentState) -> dict:
    amount = state["refund_amount"]
    if amount > 500:
        # 中断图执行,等待人工审批
        decision = interrupt({
            "type": "approval",
            "amount": amount,
            "reason": "退款金额超过自动审批阈值 500 元",
        })
        if decision != "approved":
            return {"messages": [("assistant", "退款申请已提交人工审核。")]}
    return execute_refund(state)

interrupt 会暂停图的执行并把状态持久化,人工审批后通过 Command(resume=...) 恢复。这套机制的关键是中断点必须可序列化,且恢复时能精确回到中断的位置。

人机协同的设计要点:

  • 审批粒度:不是每一步都审批(那就不叫自动化了),而是只在「不可逆 + 高价值」的操作前审批。
  • 审批上下文:给审批人看到足够信息(Agent 的推理、涉及的金额、历史操作),而不是一个孤零零的「同意/拒绝」。
  • 超时策略:审批超时后应该保守处理(默认拒绝或转更高层级),而不是默认通过。
  • 审计留痕:每次审批的决策人、时间、理由都要记录,这是合规要求。

9. 编排的可观测与成本控制

编排层的可观测与单次调用不同:它关心的是「结构」而非「单点」。核心指标:

指标口径健康值异常含义
每会话步数节点执行次数3 - 8超 12 说明绕路或循环
委派深度子 Agent 嵌套层数1 - 2超 3 说明委派失控
工具重复率重复调用占比< 5%上升说明循环倾向
中断率interrupt 触发占比5% - 20%过低说明审批形同虚设
每会话成本token 数乘单价按预算超预算需排查

成本控制的关键是预算前置:在会话开始时就分配一个 token 预算,每步扣减,接近预算时触发降级(换小模型、简化提示、强制收尾)。

class Budget:
    def __init__(self, max_tokens: int):
        self.max_tokens = max_tokens
        self.used = 0

    def spend(self, tokens: int) -> None:
        self.used += tokens

    @property
    def remaining(self) -> int:
        return self.max_tokens - self.used

    def should_degrade(self) -> bool:
        return self.remaining < self.max_tokens * 0.2      # 剩余不足 20%

编排层打 span 的具体做法、四层 span 结构与采样策略,参见 Agent 服务的可观测性与链路追踪 。要点是:每个节点一个 span,委派关系用父子 span 表达,Agent 间消息作为 span 属性而非独立 span。

10. 生产级编排的落地清单

从能跑到能运维,需要补齐以下能力:

  1. 状态定义成显式的 schema,所有节点只读写这个状态。
  2. 接持久化 checkpointer,支持任意节点恢复。
  3. 每个工具独立超时 + 熔断,单点失败不升级。
  4. 加 LoopGuard,步数与重复双上限。
  5. 关键写操作前插入 interrupt 审批。
  6. 会话级 token 预算 + 降级策略。
  7. 每个节点打 span,委派用父子 span。
  8. 提示与工具集版本化,变更走回归评测。

这份清单里,第 2 与第 4 条是最容易被跳过、又最致命的:没有检查点,一次重启丢掉全部进度;没有循环防护,一个病态输入就能烧光预算。

权衡取舍

决策点简单侧复杂侧判断依据
单体 vs 多 Agent单体 + 好提示多 Agent 分工是否需要角色权限隔离
控制流提示词驱动图式编排分支循环是否复杂
状态内存Postgres 检查点是否需要恢复与审计
工具暴露全量路由 / 子 Agent工具数量与 token 预算
记忆共享黑板共享记忆库会话长度与跨会话需求
审批无关键节点 interrupt操作是否不可逆

原则:能用代码控制的,就不要交给模型。确定性越强的环节(分类、路由、校验),越应该写成节点而非提示;只有真正需要语义判断的环节,才交给模型决策。

常见坑清单

  • 过早引入多 Agent:单 Agent 加好提示能解决的任务上了多 Agent,复杂度和成本翻几倍。先穷尽单 Agent。
  • 把控制流写在提示里:用自然语言描述「如果 A 则走 B」,模型经常不遵守。分支循环用图式编排表达。
  • 没有检查点:服务重启或网络抖动导致长任务从头再来。长任务必须接持久化 checkpointer。
  • 循环无防护:Agent 反复调用同一工具直到烧光预算。用状态指纹加步数上限双保险。
  • 委派无深度上限:子 Agent 再委派子 Agent,深度失控。限制委派层级为 1 到 2 层。
  • Agent 间传原始材料:把完整文档在 Agent 间传递,token 消耗成倍增长。只传结论与必要摘要。
  • 审批超时默认通过:人工没及时响应导致高风险操作被放行。超时应保守拒绝或升级。
  • 超时只有全局一个值:单工具慢拖垮整个会话。按层设置模型、工具、单步、会话四级超时。
  • 检查点存全量消息:每步都存几万 token 的消息列表,存储与序列化开销爆炸。只存必要状态。
  • 多 Agent 涌现行为未测:上线后出现两个 Agent 互相推诿、无限对话。必须有端到端会话级测试。

小结

编排结构的选择应该由任务确定性倒推:确定的部分(分类、路由、校验、审批)用代码控制,不确定的部分(语义判断、内容生成)交给模型。ReAct 适合简单工具调用,Plan-Execute 适合多步骤任务,Reflexion 适合可重试的场景,图式编排适合有复杂分支循环的生产系统。多 Agent 只在需要角色权限隔离或并行探索时才值得,且要付出 2 到 10 倍的成本。

工程上最重要的四件事:状态显式化(单一事实来源)、检查点持久化(支持恢复)、循环防护(步数加重复双上限)、预算前置(token 预算加降级)。这四件事决定了 Agent 是「能演示」还是「能运维」。

最后一条判断:当你发现自己在为 Agent 之间的行为写越来越多「约束提示」时,说明控制流该从提示迁移到代码了。模型的职责是决策,不是执行控制流。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「LLMOps」更多文章

  1. 语义缓存与 Prompt 缓存
  2. 结构化输出与函数调用
  3. LLM 护栏与提示注入防护