LangChain系列之多 Agent:拆之前,先回答这 3 个问题

2026-09-17 21:590 条评论196 次阅读

多 Agent:拆之前,先回答这 3 个问题

上一篇我们把工具和章法都装进了同一个 Agent。装完之后,一个新问题冒出来了。

一个 Agent 装得下 7000 个工具,装得下无数条章法,可是它一次只长一个脑子。

于是我们把多 Agent 方案认真评估了一遍。评估完,结论是先不全上。

一、一次架构评审,我们没立刻上多 Agent

事情起于产线要演进。

现在的形态是单 Agent 加 MCP 注入,能力层已经打通,工具从货架网关按需拿。下一步想做的事是:把它拆成多个专家,各自带一块记忆。

听起来顺理成章。拆开之后每个专家只看自己那一摊,工具也少了,提示词也短了。方案画出来很漂亮。

真要动手前,我先把账算了一遍。算完就停住了。

同一套合成工具,同一件业务请求,我量了两笔账。

第一笔,一次请求只要动一个专家。 单 Agent 走 3 次模型调用,输入 3292 token。多 Agent 走 4 次调用,输入 1672 token。多 Agent 只要它的 51%。

第二笔,一次请求要动三个专家。 单 Agent 一轮并发发 3 个工具调用,2 次调用、2232 token 就完事。多 Agent 串行转交,10 次调用、3601 token,涨到 161%。

同一个架构,两种任务,结论正好相反。

差别只有一个:你的一次请求要动几个专家。

所以问题从来都不是「多 Agent 好不好」。它是「你的一次请求,要动几个专家」。

这篇就沿着这两笔账往下问。多 Agent 到底买到了什么,它凭什么值这个价,以及什么时候它其实一分钱都不值。

先说结论:多 Agent 不是能力升级,它是一种花隔离钱的手段。

二、多 Agent 到底买的是什么

先把一个误会拆掉。

拆成 4 个 Agent 之后,你可能期待模型变聪明了。事实是模型还是那个模型。每个专家手上的工具,本来就是原来那些工具的子集。提示词短了一点,选择少了一点,推理能力一点没长。

那多出来的是什么?

第一件是上下文隔离。 订单专家不需要看到知识库的检索结果,报表专家也不需要看到退货政策的原文。让每个专家只看自己那一摊,它的判断就不会被隔壁的话题带偏。

第二件是权限隔离。 一个 Agent 能改库,另一个只能读。这事在单 Agent 里做不到,因为工具是同一个模型手里的,模型只要愿意就能调。

第三件是失败域隔离。 报表专家今天接口挂了,订单链路还能照常跑。单 Agent 里一处工具报错,整条推理都可能断在半路。

我把这三件和它不卖的东西放在一张表里。

多 Agent 卖什么 多 Agent 不卖什么
上下文互不污染 更聪明的模型
权限物理可分 更低的 token 成本
失败域互相隔开 更少的调试工作
每个专家的提示词更短 更少的模型调用次数
职责边界写在图里,而不是写在提示词里 免掉记忆归属的麻烦

最后一格值得单独说一句。很多人以为拆开之后事情变简单了,实际上多 Agent 会新增一个单 Agent 里不存在的问题:记忆放在哪。

这个问题后面第八章专门讲。

所以判断标准其实收得很窄。它只有一句话:你有没有必须隔离的东西。

如果你答得出来,多 Agent 是一笔划算的采购。如果答不出来,你花掉的是调用次数、调试时间和一堆新的失败模式,换回来的是三个你并不需要的隔离。

三、先说清楚:默认情况下,它并不隔离

这里有个反直觉的地方,是本篇最想讲的一件。

很多人以为「把 Agent 拆开」这个动作本身就带来了隔离。实测下来,LangGraph 默认并不给你隔离。

我写了一个记录型的假模型,它会把每次收到多少条消息、什么角色,全记下来。然后用它搭最简单的场景:父图里嵌一个子 Agent,父图先攒了 24 条历史消息。

from langchain.agents import create_agent
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from typing_extensions import TypedDict
from typing import Annotated


class State(TypedDict):
    messages: Annotated[list, add_messages]


sub = create_agent(model=spy, tools=[], system_prompt="你是订单专家。")

g = StateGraph(State)
g.add_node("sub", sub)          # 子 Agent 直接当节点
g.add_edge(START, "sub")
g.add_edge("sub", END)
app = g.compile()

app.invoke({"messages": history + [HumanMessage(content=TASK)]})

跑完看记录。

>> 父图历史:24 条消息,1984 字符 = 612 token
>> 子 Agent 第一次调用看到 26 条消息
>> 第一次调用的输入 = 647 token
>> 其中历史占比 = 94.6%

子 Agent 收到了父图已有的全部消息,包括那 1 条 system 和 24 条历史。它的回复也会回灌进父图的 state["messages"]。

在这个姿势下,多 Agent 只是把「同一个大上下文」切成了几次请求。一分钱没省,一点没隔离。

父图 state.messages
24 条历史

子 Agent 节点

子 Agent 看到全部 26 条
输入 647 token

回复回灌父 state.messages

那真正隔离要怎么做?3 种手段,都实测过。

手段一,用 Send 只传挑出来的那几样。 Send("worker", {"task": ...}) 送到 worker 节点的是一个全新的 payload,父图的历史一概进不去。

>> worker 第一次调用看到 2 条消息(system + 任务)
>> 输入 = 35 token
>> 对比默认姿势:647 → 35,省下 95%

同一条任务,647 token 变成 35 token。省掉的 612 token 全是父图那份跟本次任务无关的历史。

手段二,给子图声明自己的 state schema。 这个手段有用,但只挡得住名字不同的键。

>> 子图实际收到的 state 键 = ['messages']
>> 子图收到的 messages 条数 = 25
>> 父图的 query 有没有进去:False

父图里的 query 因为子图没声明,被挡住了。可是 messages 这个名字两边都有,于是整段历史照样进了子图。

这是个很容易踩的坑。你以为声明了独立 schema 就隔离干净了,其实只要键名撞上,数据就一路畅通。

手段三,控制回传量。 隔离是双向的。子 Agent 收到了什么要管,它交回什么也要管。这一条在第四章用 output_mode 实测。

把三者的关系画出来:

真隔离:三选一或叠加

Send 只传任务

独立 schema
挡住异名键

output_mode
控制回传量

父图

worker
35 token

父图

子图

父图

子 Agent

默认姿势:不隔离

父图全历史

子 Agent

一句话收口:在 LangGraph 里,隔离不是拆出来的,是自己写出来的。

顺手说一下为什么会这样。

子 Agent 作为节点嵌进父图时,它的输入 state 是从父图的 channel 里取的。两边名字一样的键就直接对上,父图有 messages,子图也认 messages,于是整段历史顺着这个名字流了过去。名字不一样的键,子图的 schema 里没有位置,就停在外面。

这不是设计缺陷,是子图的默认语义。图嵌图的目的是复用,不是隔离。 想要隔离,就得在数据交接的地方动手。

四、官方给了两个预制件,先把它们跑起来

自己画之前,先看看官方给的东西能省多少事。

langgraph-supervisor 和 langgraph-swarm 这两个包,版本号看着有点劝退:一个是 0.0.31,一个是 0.1.0。而 langgraph 本体早就 1.x 了。

我一开始也怀疑它们是不是没跟上。翻依赖声明才发现不是这么回事。

包 版本 依赖声明
langgraph-supervisor 0.0.31 langgraph>=1.0.2,<2.0.0、langchain-core>=1.0.0
langgraph-swarm 0.1.0 langchain>=1,<2、langgraph>=1,<2

两个包都明确声明支持 1.x。 版号小不代表不能用,只代表它们的 API 还没冻结。

实测也确实跑得起来。

主管式:supervisor 实测

先看它编译出来的图长什么样。

from langgraph_supervisor import create_supervisor

wf = create_supervisor(
    [order_agent, kb_agent, report_agent],
    model=supervisor_model,
    prompt="你是主管。",
)
app = wf.compile()

拓扑很规整:__start__ → supervisor,supervisor 三条条件边分别指向 __end__ 和各子 Agent,子 Agent 无条件回边到 supervisor。

主管手里的工具长这样:['transfer_to_order_expert', 'transfer_to_kb_expert', 'transfer_to_report_expert']。一个专家配一个转交工具。

接着我读了一遍转交工具的源码。有几件事跟想的不一样。

@tool(name, description=description)
def handoff_to_agent(
    state: Annotated[dict, InjectedState],
    tool_call_id: Annotated[str, InjectedToolCallId],
) -> Command:
    ...
    return Command(
        goto=agent_name,
        graph=Command.PARENT,
        update={**state, "messages": handoff_messages},
    )

它不接收 task 参数。 签名里只有注入的 state 和 tool_call_id。也就是说,主管没法在转交的时候捎一句「你要做的是某某事」。

它把整份 state 交给目标 Agent。 update={**state, ...} 这一行说明了问题:转交过去的是完整状态,包括全部 messages。

这正好解释了第三章的实测。预制件转交这一路,走的就是「父图全历史都带过去」的模式。

还有一处更要紧的。

if description is None:
    description = f"Ask agent '{agent_name}' for help"

默认描述就是这一句:Ask agent 'order_expert' for help。

模型要在几个专家之间做选择,靠的就是工具描述。而官方默认给的这句,除了专家名字之外没有任何区分信息。真上手用,这套描述必须自己写。

一次转交,账是这么走的

我抓了一次真实转交的完整轨迹,7 条消息。

human
  → ai(transfer_to_web_agent)
    → tool(Successfully transferred to web_agent)
      → ai(专家作答)
        → ai(transfer_back_to_supervisor)
          → tool(Successfully transferred back to supervisor)
            → ai(主管收口)

子 Agent 侧会自动生成一个回程工具 transfer_back_to_supervisor。专家答完,主动把控制权交回去。

一次转交来回,消息池净增 4 条。 主管问一句、专家答一句,中间夹着两条工具回执。这个成本在第七章会放大得很难看。

交回多少,官方给了个旋钮

create_supervisor 有个 output_mode,默认值是 last_message。

取值 主管第二次调用看到的 messages
last_message(默认) 7 条
full_history 9 条

差别就在子 Agent 内部那轮工具调用。默认只把它最后的结论交回来,full_history 会把中间的 tool_call 和工具结果一起交回。

这是官方给的「回传量」控制手段,跟第三章的手段三是一回事。

交接式:swarm 实测

langgraph-swarm 的图是另一种形状。它没有中央主管节点,节点就是各 Agent 自己。

__start__ 两条条件边,按 default_active_agent 决定谁先上场。之后 Agent 之间直接互转,谁接到谁处理。

有个细节值得记一下。create_swarm 的 state 叫 SwarmState,但实测 invoke 出来以后,返回的状态键只有 ['messages'],当前活跃 Agent 那个字段不在里面。

这意味着你没法在跑完之后从结果里直接读出「最后是谁在说话」。想知道,得去翻消息或者看 trace。这是交接式形态的一个具体不便。

两个包的公开导出也放在这里,装完可以先看一眼它们给了什么。

包 导出
supervisor create_supervisor / create_handoff_tool / create_forward_message_tool / handoff / supervisor / agent_name
swarm create_swarm / create_handoff_tool / add_active_agent_router / SwarmState / handoff / swarm

swarm:无中央节点

按 default_active_agent

互转

__start__

alice

bob

__end__

supervisor:有中央调度

transfer

transfer

无条件回边

无条件回边

__start__

supervisor

order_expert

kb_expert

__end__

一个我没想到的坑:并行转交会丢任务

create_supervisor 还有一个参数叫 parallel_tool_calls。我的第一反应是:打开它,主管就能一轮发几个转交,几个专家同时干活。

先看默认值。

parallel_tool_calls  default=False
output_mode          default='last_message'

默认关着。那打开呢?

我做了一组对照,让主管一次发出 2 到 3 个转交工具调用。结果两件事一起冒出来。

第一件,工具全都正常执行了。 我给转交工具装了探针,三个都跑了,返回值也一个没错:

transfer_to_kb_expert      graph=__parent__  goto=['Send:kb_expert']
transfer_to_order_expert   graph=__parent__  goto=['Send:order_expert']
transfer_to_report_expert  graph=__parent__  goto=['Send:report_expert']

第二件,父图只接管了一个。 三个工具都正确地问了「派我去哪个专家」,父图最后只执行了 1 个专家的节点,另外 2 个静默消失。

我试了 parallel_tool_calls 关和开两种,试了发 2 个和发 3 个转交,一共 5 次,每次都是只落地 1 个。规律还挺整齐:生效的总是最后被调用的那个转交。

没有任何报错。日志上看不出丢了东西,只有结果少了两块。

翻了一下 langgraph 的 ToolNode._combine_tool_outputs,合并逻辑要求这些 Command 的 goto 都是纯 Send 列表才归并成一个父命令。至于为什么最后只有一个落地,我没能在这轮里追到根上。

但对你来说,根因是哪一层不重要,现象本身才是要记住的那件事:在这套版本组合下,别指望「主管一轮发多个转交」来实现并行派活。

想并行,就自己在图上用条件边加 Send 扇出去。这就是第五章要写的东西。

五、自己画:Command 与 Send 两条路

预制件装不下的场景,得自己画。原语其实只有两个。

交接:Command

from langgraph.types import Command

def order_agent(state):
    ...
    return Command(
        goto="kb_agent",                    # 控制流交给谁
        update={"messages": [...]},         # 顺手更新什么状态
        graph=Command.PARENT,               # 往父图跳
    )

graph=Command.PARENT 是给「我在子图里、要往父图跳」这个场景用的。实测 Command.PARENT 的值就是一个普通字符串:

>>> Command.PARENT
'__parent__'

Command 是 dataclass,不是 pydantic 模型。它的字段有 graph、update、goto、resume。

这里有个容易搞混的分工。 Command 改的是「控制流去哪」,Send 改的是「这一份活派给谁、带什么数据」。

并行分发:Send

Send 的用法不复杂,但它有个位置上的硬要求。

from langgraph.types import Send

# 正确:从条件边返回
def dispatch(state):
    return [Send("worker", {"seed": "A"}),
            Send("worker", {"seed": "B"}),
            Send("worker", {"seed": "C"})]

g.add_conditional_edges("dispatch", dispatch, ["worker"])
# 错误:写在普通节点里
def fan_out(state):
    return [Send("worker", {"seed": "A"})]     # 直接抛错

g.add_node("fan_out", fan_out)

第二种写法在 1.x 上的报错原文是:

langgraph.errors.InvalidUpdateError: Expected dict, got [Send(node='worker', arg={...}), ...]

Send 必须从条件边返回。 网上不少示例把它写在节点体里,在这个版本上跑不通。

实测三个 Send 各带 {"seed": "A"/"B"/"C"},三个 worker 各拿到自己的 payload,互不干扰。这是第三章手段一在并行场景下的形态。

两个原语的对照

原语 改什么 从哪返回 典型场景
Command(goto=...) 控制流去哪 节点或工具 交接给下一个专家
Send(node, payload) 活派给谁、带什么 只能从条件边 同一个活并行发给多个 worker

预制件和原语的关系也说一句。预制件是给人快的,原语是给人改的。 前面那两个包本质上是把这几个原语按一种固定套路拼好了。套路对得上,用预制件省事;对不上,回来用原语自己拼。

顺便说一个:destinations 防不了环

转交环是多 Agent 最典型的翻车方式,所以我特别想找一个官方开关。

add_node 在 1.x 里加了一批新参数:input_schema、destinations、error_handler、timeout、defer、cache_policy、trace_policy。其中 destinations 看起来最像「限制这个节点能跳到哪」。

我试了 4 种写法。

写法 结果
不声明 destinations,goto 自由跳 跳到环上,撞 GraphRecursionError
声明 destinations=("b",),实际 goto "b" 一样撞 GraphRecursionError
声明 destinations=("c",),实际 goto "b" 照样跳到 b,一样撞环
声明 destinations=() 空清单 照样跳
声明一个不存在的节点名 编译期报 ValueError: Found edge ending at unknown node

destinations 不做运行时校验。 它声明的是「这个节点可能去哪」,编译时会拿它建边、检查节点存在,但管不住 Command 在运行时往哪跳。

所以防环还得靠自己。这个坑在第七章给方案。

六、三种形态怎么选

前面把两个预制件和自己画的路线都跑了一遍。现在收成一张选型表。

形态 谁在调度 适合什么场景 代价
主管式(supervisor) 中央主管 Agent 一开始不知道这事该归谁 每轮多一层往返,主管模型得够聪明
交接式(swarm) Agent 之间直接互转 谁接到谁处理的流式协作 没有中央视角,出问题不好归因
自绘 你自己画的图 流程本来就确定的场景 回退与循环得自己兜

再给一张「症状对形态」的表,这个更实用。

你遇到的情况 该用的形态
用户提问开放,我先得判断这归哪个专家管 主管式
每个专家的能力边界很清楚,谁先接手可以按规则定 交接式
流程是死的(订单流转、审批链),分支条件可枚举 自绘
有一步必须并行扇出去再收回来 自绘,条件边加 Send
我只是想让提示词短一点 先别拆,看第九章

一条经验顺序:能自绘就别用预制件,能用预制件就别硬编。

这个顺序反了会浪费很多时间。见过有人拿着一套三行代码就能表达的固定流程,硬套主管式,最后在转交描述和环检测上花了几天。

反过来也成立。如果场景就是「用户随口一问,我不知道该谁答」,自绘一张图要先写一个分类器,那还不如直接用主管式。

至于主管式和交接式的取舍,可以问自己一句话:我需要一个能看见全局的人在吗? 需要就主管式,不需要就交接式。

七、代价账单:动一个省,动三个贵

这一章是这篇的主场。把三种代价一次算清。

第一笔:固定开销

固定开销指的是跟对话无关、但要被重复注入的部分:系统提示词加工具定义。

形态 系统提示词 工具定义 合计
单 Agent(6 个工具) 162 872 1034
多 Agent(1 主管 + 3 专家) 322 927 1213

多 Agent 的固定开销是 1.17 倍。拆开之后,多了一份主管的提示词和 3 个转交工具。

这个倍数不算吓人,因为它不随对话变长而增长。真正难看的是后面两笔。

第二笔:窄任务,多 Agent 赢

同一件只动 1 个专家的事,比如「查一下订单,顺手试试能不能退」。

形态 模型调用 输入 token 倍数
单 Agent 3 次 3292 基准
多 Agent 4 次 1672 0.51x

多 Agent 省了一半。

原因不复杂。单 Agent 每次请求都要带上那 6 个工具的定义,哪怕这次只用得上订单相关的 2 个。多 Agent 里,订单专家手上只有自己那两三个工具,每次请求都轻。

第三笔:宽任务,多 Agent 输

同一套工具,换成一件要动 3 个专家的事:查订单、查知识库、出报表。

形态 模型调用 输入 token 倍数
单 Agent 2 次 2232 基准
多 Agent 10 次 3601 1.61x

单 Agent 一轮并发发 3 个工具调用,2 次调用就收工。多 Agent 串行转交了 3 次。

调用次数的差距比 token 更刺眼:2 次变成 10 次。

两次的账放在一起看:

一次请求
动几个专家?

1 个

3 个

多 Agent 0.51x
省一半

多 Agent 1.61x
贵一半

分水岭就是这一句:一次请求要动几个专家。

中间那段我没测。1 个专家的时候多 Agent 赢,3 个专家的时候多 Agent 输,2 个专家落在哪一侧,取决于你的工具定义有多重。

道理不难推。工具定义越重,单 Agent 每次请求背的包越大,多 Agent 的省就越明显;工具定义轻而转交次数多,多 Agent 输得越难看。

所以别抄别人的倍数,用你自己的工具算一遍。要量的只有两个数:你的一次请求平均动几个专家,你的单工具定义平均多少 token。 前一篇文章给过单工具 220 token 左右的实测均值,那个数拿来当起点就够用。

有一处我说明一下口径。宽任务里多 Agent 走的是串行转交,10 次调用。supervisor 号称支持并行转交,但第四章实测下来,这套版本上并行转交会丢任务。所以这 10 次可以当成这套组合下的实际数字。

除了 token,还有三笔隐形账

第一笔,消息膨胀。 我故意让主管每次都转交,抓了它的轨迹。

轮次 主管侧消息数 专家侧消息数
第 1 次转交 2 3
第 2 次转交 7 8
第 3 次转交 12 13
第 4 次转交 17 18

每来回一次,双方各涨 5 条消息。 而每一轮请求都要把这些消息重新付一次钱。

第二笔,转交死循环。 上面那个实验第 8 次就撞墙了:

langgraph.errors.GraphRecursionError: Recursion limit of 8 reached without hitting
a stop condition. You can increase the limit by setting the recursion_limit config key.

主管每次都把活推出去,专家每次都推回来,图就这样转到天荒地老。这是主管式编排最典型的翻车方式,而且它在真实环境里的触发条件比实验里脏得多:专家答得含糊、主管判断不清、两个专家互相觉得该对方管。

第三笔,调试成本。 一个用户请求穿过 4 个 Agent,trace 里全是嵌套 span。单 Agent 出问题,你看一段对话就知道哪错了;多 Agent 出问题,你得先搞清楚这一段是哪个 Agent 说的,再搞清楚它为什么被叫起来。

检查点也在涨

顺手量了一个前面几篇没量过的数:一轮对话要存多少检查点。

形态 一轮消息数 检查点条数
单 Agent(提问 → 调工具 → 作答) 4 5
多 Agent(1 主管 + 3 专家各跑一轮) 8 21

多 Agent 的检查点是单 Agent 的 4.2 倍。它给每个子图单独分了 namespace,父图 12 条,3 个专家各 3 条。

会话不长的时候这个数字无所谓。但如果你按条数计费的存储上跑长会话,这是一笔会慢慢显形的账。

八、记忆放在哪:多 Agent 之后,位置变了

前面那篇讲记忆的时候,store 是一个很简单的东西:一个共享的记忆库,按 namespace 隔离用户。

多 Agent 之后,它变成了一个需要做决定的问题。

我先说三件事,都是实测出来的。

第一件,父图的 store 会自动透传进子 Agent。 子 Agent 里的工具用 get_store() 就能拿到父图编译时传进去的那个 store。哪怕专家是在 Send 扇出去的并行分支里跑的,也拿得到。

@tool
def save_note(key: str, value: str) -> str:
    """把一条记忆写进长期记忆库。"""
    st = get_store()
    if st is None:
        return "store 是 None"
    st.put(("probe", "u1"), key, {"value": value})
    return f"已写入 {key}"

第二件,子 Agent 自己带一个 store,在父图里不生效。 create_agent 确实有 store 参数,我本来以为这就是「一个专家一个 store」的官方做法。

实测三次,结论是反的。

场景 工具里摸到的 store
父带 store,子 Agent 也带自己的 父的生效,子自己的空的
父不带 store,子 Agent 自己带 store=None,一个都没有
子 Agent 不嵌父图、独立运行 自己带的那个生效

只要专家是被父图当节点调起来的,store 就由父图说了算。 想让每个专家各拿一个独立 store,得走别的路,比如让专家作为独立服务单独部署。

第三件,检查点会自动分家。 父图的 saver 会给每个子 Agent 开一个自己的 namespace,子 Agent 自己带不带 checkpointer 都一样。

这三件事合起来,把选项收窄了。多 Agent 的记忆归属实际只有两种做法。

做法 怎么实现 好处 代价
共享一个 store 父图传 store,各专家用 namespace 区分 同一个用户跨专家有连续认知 namespace 设计错了会串味,权限也难分
主管持有,按需注入 专家无状态,主管读写记忆后写进 payload 最可控,记忆入口唯一 最贵,每次都要主管做一次记忆读写

主管持有

把相关记忆写进 payload

只传必要片段

用户

主管
读写 store

订单专家
无状态

知识专家
无状态

共享 store

用户

父图 store

订单专家
ns=order/u1

知识专家
ns=kb/u1

我产线里现在走的是混着来的做法,可以给你参考。

共享 store 里放的是那些「跨专家必须一致」的东西:用户身份、会员等级、偏好语言、历史订单归属。这些一旦不一致,用户立刻能感觉出问题。

各自持有的是各专家的专业上下文:订单专家记住这个人常买什么品类,知识专家记住他问过哪类政策问题。这两块互相冲突也没关系,因为它们本来就不该混着用。

最要小心的是「用户偏好」这一类。 如果订单专家和知识专家各记了一份这个人喜欢什么,第一次出现冲突的时候,你可能都不知道该信谁。

九、什么时候不该上多 Agent

这一章给判决。三个问题,任何一问答不出「是」,就先别拆。

第一问,有没有必须隔开的上下文? 不同职责看到的东西,本来就该不一样吗?如果所有专家都需要看同一份完整对话,那拆开只会让大家都不完整。

第二问,有没有必须分开的权限? 一个能改库、一个只能读,这个要求能不能用工具层面的校验实现?如果能,就不必为它拆 Agent。

第三问,有没有必须独立的失败域? 一个专家挂了,整条链路能不能容忍一起挂?

再拆掉三种最常见的「以为需要多 Agent」。

你以为的问题 真正的问题是 该用的手段
工具太多,7000 个管不动 工具规模化 货架网关,按需可见(见《MCP 与 Skills 接入》)
规则太多,一个人格装不下所有章法 过程知识的组织 Skills,按需读取(同上)
步骤太多,怕一个 Agent 走不完 流程编排 子图与条件边(见《LangGraph 进阶》)
上下文太长,塞不下 消息历史治理 裁剪、删除、摘要三招(见《Agent 的记忆》)
两类信息不该互相看见 真正的隔离需求 多 Agent

最后一行才是多 Agent 的入场券,前面四行都有更便宜的解法。

有一处我想说清楚。这些便宜解法不是「退而求其次」,而是它们本来就治那个病。拿工具太多这个例子说,你拆成 3 个专家,每个专家手上 2000 个工具,模型照样选不动。它换的只是把一个大问题切成 3 个中等问题。

收口一句:多 Agent 是为隔离付钱,不是为了看起来专业。

十、收尾

最后给两张清单。

拆之前,先做完这 4 件事:

  1. 算清你的一次请求平均要动几个专家。宽任务占多数的话,先别拆
  2. 确认要隔离的到底是什么。说不出具体的两项,先别拆
  3. 想清楚记忆放哪。共享还是主管持有,这个决定要写在架构图里
  4. 给失败兜底。recursion_limit 要给一个明确的值,别用默认

拆之后,守住这 4 条:

  1. 转交工具的 description 必须自己写。官方默认的 Ask agent 'xxx' for help 里没有判断信息
  2. 转交链路要防环。计数守卫加循环检测,别指望 destinations
  3. 回传量要控。output_mode 是官方给的旋钮,默认值未必适合你
  4. trace 要能一眼看出穿了哪几个 Agent。做不到这一点,多 Agent 的调试成本会翻倍

再回到上一篇的位置。上一篇我们把工具和章法装进了同一个 Agent,装得很满。这一篇回答的是,什么时候值得拆开装。

答案不在「多 Agent 先不先进」,在你手上有没有必须隔开的东西。

如果三问答不出「是」,就先别拆。不是不能拆,是没必要为一份你用不上的隔离,付一笔收不回来的钱。


下一篇,我们聊聊多 Agent 真的上线之后,怎么知道它到底跑得好不好。

回复 multiagent,领本篇完整的工程化 demo。