当团队把单 Agent 跑通之后,下一个冲动通常是「上多 Agent」。这个冲动需要被克制。多 Agent 不是能力的线性叠加,而是复杂度的指数上升:调试从「看一条链路」变成「看多个 Agent 之间的对话」,成本从「一次调用」变成「N 个 Agent 各自循环」,失败模式从「模型答错」变成「两个 Agent 互相推诿、或者无限对话」。绝大多数业务用单体 Agent 加工具就够了,多 Agent 只在明确需要「角色分工」或「并行探索」时才划算。
本文与 Agent 服务的可观测性与链路追踪 划清边界:那篇讲的是 Agent 上线后怎么监控、怎么打 span、怎么告警;本文讲的是编排结构本身——状态怎么定义、控制流怎么走、子 Agent 怎么委派、失败怎么恢复。前者是「看得见」,后者是「搭得对」。
编排的本质是把不确定的模型决策,放进一个确定的控制结构里。ReAct 循环是最简单的结构,图式编排(LangGraph 类)是最灵活的结构,多 Agent 是最重的结构。选择的依据是任务的确定性程度:越确定的任务,越应该用代码而不是模型来控制流程。Agent 的基础构件(模型、工具、记忆、循环)可先参考 Agent 架构 建立整体认知。
目录
- 单体 Agent 与多 Agent 的取舍
- 编排范式:ReAct、Plan-Execute 与 Reflexion
- 图式编排与状态机
- 检查点、恢复与持久化
- 工具路由与子 Agent 委派
- 多 Agent 通信与共享记忆
- 失败重试、超时与死循环防护
- 人机协同与审批节点
- 编排的可观测与成本控制
- 生产级编排的落地清单
- 权衡取舍
- 常见坑清单
- 小结
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())
图式编排的三个价值:
- 控制流显式化。分支、循环、并行都用边表达,代码可读、可测试、可画出来。
- 状态是单一事实来源。所有节点读写同一个
AgentState,避免散落在各处的全局变量。 - 天然支持恢复。图是有状态的,配合 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. 生产级编排的落地清单
从能跑到能运维,需要补齐以下能力:
- 状态定义成显式的 schema,所有节点只读写这个状态。
- 接持久化 checkpointer,支持任意节点恢复。
- 每个工具独立超时 + 熔断,单点失败不升级。
- 加 LoopGuard,步数与重复双上限。
- 关键写操作前插入 interrupt 审批。
- 会话级 token 预算 + 降级策略。
- 每个节点打 span,委派用父子 span。
- 提示与工具集版本化,变更走回归评测。
这份清单里,第 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 之间的行为写越来越多「约束提示」时,说明控制流该从提示迁移到代码了。模型的职责是决策,不是执行控制流。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。