“多智能体”这几年有点被当成“AI应用进阶的标配”了,好像不拆出三五个Agent就显得不够高级。我的观点反而比较朴素:多智能体不是用来炫技的,它是当单个Agent的复杂度跨过某个临界点之后,不得不做的工程拆分。真正决定一个系统能不能上线、能不能长期维护的,不是Agent的数量,而是Agent之间的编排关系。
LangGraph这个框架我实际跑过几个项目之后,感受最深的是它把“图”这种编排方式落到了工程细节上。节点怎么连接、状态怎么流转、路由怎么收敛,全部变成代码里可以控制和调试的东西。这篇文章不聊“什么是Agent”这类基础课,直接聚焦我在项目里最高频使用的两种设计模式:主管架构(Supervisor)和分层架构(Hierarchical)。如果你已经能用LangGraph搭出能跑的单Agent,但是做多Agent编排时总觉得状态混乱、路由不稳、上下文爆炸,这篇文章会给你一套可以直接套用的结构思路,以及我实打实踩过的坑。
1. 先别急着加Agent:单Agent的边界在哪里
1.1 单Agent的隐性成本
很多人觉得“一个Agent挂20个工具,不也是智能体吗”,这是个不小的误区。
单一Agent本质上是让同一个模型在同一个system prompt里同时处理所有工具的选择、参数拼接、结果判断和下一步决策。当工具只有三五个的时候,这条路很顺。但工具数量一旦涨到15个以上,有几个问题会紧接着冒出来:
- prompt里工具描述挤占大量上下文窗口,真正给用户对话和中间结果的空间越来越少。
- 模型在不同工具之间做选择时,容易出现“看着哪个都像”的误判,尤其两个工具功能边界相近的时候。
- 某个工具的调用规范改了,你可能要连带改一大段prompt,结果另一个原本正常的功能也开始偶尔抽风。
- 所有工具产生的中间结果都堆在同一个上下文里,互相干扰,模型分不清哪些信息跟当前任务有关。
你当然可以把一个Agent硬撑下去,但后果通常是准确率忽高忽低、调试成本越来越大、升级模型版本后行为还不可控。这跟开发里“一个类写到两千行”是同一个味道:能跑,但已经很难改了。
1.2 决定拆分的四个信号
我自己判断“该拆了”,基本看下面四个信号,出现两三个就动手:
- 工具数量超过15个,或者工具说明文本量超过5000字,路由误判率开始明显上升。
- 不同工具集面对的领域差异很大。比如一个Agent既要写代码,又要查审批流程,还要处理图片,这就是典型的不同“专业方向”。
- 你需要对不同类型的任务做不同的上下文压缩策略。比如“读文档”的任务只需要保留摘要,“写代码”的任务需要保留完整文件内容。
- 单个Agent在某个子任务上卡住时,整个会话都得跟着重来,你没法只重跑那一段。
这四点里,第三点最容易被忽略。很多人觉得“统一塞进messages列表就行”,但不同子任务对信息的敏感度天差地别:让负责搜索的Agent看到大量代码差异没有意义,让负责代码审查的Agent看到一堆网页摘要也只会让它犯迷糊。
1.3 先画调用关系图,再谈架构
我每次接手这类重构,都会先用一张最简单的图把任务关系画出来。不需要画成什么标准UML,就是列出系统要处理的任务类型,然后标出它们之间有没有交叉依赖。
比如一个企业内部的信息处理系统,大概会拆成这样:
- 任务A:从各种数据源抓取内容,做清洗和结构化。
- 任务B:基于结构化内容做检索和问答。
- 任务C:把检索结果生成报告或可视化。
- 任务D:负责最终的内容审核。
如果画完发现每个任务之间是线性上下游关系,那单Agent链就够了,不需要上多智能体。如果你发现某个中心节点要反复调度其他几个模块,并且不同模块之间的信息不能完全互通,那才是真正需要用主管架构或分层架构的时候。
画这张图还有一个作用:它基本决定了你的图结构是“星型”还是“树型”。星型就是中心调度者直接连所有Worker,树型就是顶层调度者只管几个团队,团队内部再继续拆。这正好对应了接下来要讲的两种模式。
2. 主管架构:一个项目经理带一群专家
2.1 主管架构的核心:只路由,不干活
主管架构是目前中小规模多智能体系统里最常用的模式。它的结构非常直接:
- 一个Supervisor节点,负责理解用户请求、判断该找哪个Worker、根据Worker返回的结果决定下一步继续调用谁还是结束。
- 一组Worker节点,每个Worker负责一个具体领域,只在自己的工具集和上下文里干活。
- Supervisor和Worker共享一个状态对象,但Worker之间一般不直接通信。
打个比方,Supervisor是项目经理,Worker是各个岗位的专家。项目经理不需要自己写代码、做设计、跑测试,他只需要搞清楚“当前这个阶段该叫谁进会议室”。专家干完活把结果交回来,项目经理判断是否还需要其他专家介入,直到认为任务可以交付。
这种设计最大的优点是把“路由判断”和“任务执行”彻底分开了。路由逻辑集中在一个节点里,你可以专门调优它的prompt,而不用碰各个Worker的内部实现。Worker之间的职责边界也因为“不直接通信”而变得非常干净,谁也不会悄悄调用谁的私有逻辑。
这里有一个容易被新手忽略的点:Supervisor不干活,意味着它不应该持有具体工具,或者最多持有少量用于全局判断的只读工具。一旦Supervisor开始“顺手”调用某个领域工具,它很快就会变成原来的巨型单Agent,只是换了个名字。
2.2 状态设计与工具边界
主管架构里最关键的工程细节是状态结构。我实际用下来,比较稳的做法是把状态拆成两类内容:
一类是全局共享的,比如消息流、最终结果、当前正在执行的Worker名称。这类字段放在顶层状态里,所有节点都能看到。
另一类是各Worker自己的中间数据。这类数据不应该塞进顶层状态,而应该让Worker把“处理完之后的结论”返回顶层,原始中间过程留在Worker内部。
举个例子,我做过一个某公司内部的技术问答系统,其中一个Worker负责解析代码仓库,它会扫出几千个文件。如果这个Worker把每个文件的分析结果都塞回顶层状态,后面的Agent根本没法正常思考。更合理的做法是:Worker内部保留完整分析结果,只把“哪几个文件与当前问题相关、关键函数是什么、结论是什么”写入顶层状态。
本质上就是在多Agent之间建立一个“汇报制度”:下面干完活向上层汇报结论,而不是把整个工作现场搬给上层。这个原则无论主管架构还是分层架构都适用。
2.3 用LangGraph搭一个最小主管架构
LangGraph的StateGraph天然适合这种星型结构。下面是一个结构示意代码,我用简化方式来写,重点看它怎么体现“主管只决策、Worker只执行”的思路:
from typing import TypedDict, Annotated, Literal from langgraph.graph import StateGraph, END from langgraph.graph.message import add_messages class AgentState(TypedDict): messages: Annotated[list, add_messages] active_worker: str final_answer: str def supervisor_node(state: AgentState): # 这里不调用任何业务工具,只做路由判断 prompt = build_supervisor_prompt(state["messages"]) decision = llm.invoke(prompt) # 返回一个worker名称,或者"FINISH" return {"active_worker": decision.next_worker} def search_worker(state: AgentState): # 只做检索,最终返回一段精炼结论 results = run_search(state["messages"][-1].content) return {"messages": [format_result(results)]} def report_worker(state: AgentState): # 只做报告生成 report = generate_report(state["messages"]) return {"messages": [format_result(report)]} def should_continue(state: AgentState) -> Literal["search_worker", "report_worker", "FINISH"]: if state["active_worker"] == "search": return "search_worker" elif state["active_worker"] == "report": return "report_worker" else: return "FINISH" graph = StateGraph(AgentState) graph.add_node("supervisor", supervisor_node) graph.add_node("search_worker", search_worker) graph.add_node("report_worker", report_worker) graph.set_entry_point("supervisor") graph.add_conditional_edges( "supervisor", should_continue, {"search_worker": "search_worker", "report_worker": "report_worker", "FINISH": END} ) graph.add_edge("search_worker", "supervisor") graph.add_edge("report_worker", "supervisor")注意几个细节。
第一,所有Worker执行完之后都连回Supervisor,而不是连到另一个Worker。这是刻意的。如果让Worker直接连Worker,你就失去了“中转控制”的机会。比如检索完之后,到底是要写报告还是先做一轮质量检查,这个决策应该由Supervisor来做,不能让检索Worker自己决定。
第二,条件边里的判断依据是state["active_worker"],这个值由Supervisor在上一轮写入。本质上是“Supervisor写一份派工单,图框架根据派工单派活”。
第三,messages字段用add_messages作为reducer,确保每个Worker返回的消息会被追加而不是覆盖。这是LangGraph里最基础的约定,但很多人一开始会忘记。
2.4 主管架构最容易翻车的三个点
这个架构我在项目里跑过很多轮,说三个最容易翻车的地方。
第一个是Supervisor的prompt写太长。你以为写清楚每个Worker的十八种边界情况是负责任,结果模型反而抓不住重点。我的经验是,Supervisor的prompt只写三件事:每个Worker擅长什么、当前任务什么情况下应该找哪个Worker、什么情况下直接结束。其他细节全部放到Worker自己的system prompt里。
第二个是Worker返回的信息结构太随意。很多Worker喜欢直接返回一大段“原汤化原汁”的文本,结果下一轮Supervisor要花大量token去理解这段文本,还会被无关细节干扰。我后来规定所有Worker的返回值必须是“结论+来源+置信度”的结构,完了。
第三个是忘记设置递归上限。主管架构天然带循环,一旦Supervisor判断失误就可能来回切同一个Worker。LangGraph里有对递归深度的限制,但你最好在业务层也加一层防护,比如每个任务最多经过5轮路由,超过就直接转到兜底逻辑。这个在“常见问题”里还会重点讲。
3. 分层架构:多团队协作的业务单元
3.1 为什么需要第二层
主管架构摊平之后,Worker数量一旦超过十个,Supervisor就会变得不堪重负。我自己在某公司做到十一个Worker的时候,明显感觉到主管节点开始“精神分裂”——有些任务在两个相似的Worker之间反复横跳,有些任务则因为“这个不该我管”而降级到兜底逻辑。
问题不出在模型,而出在结构:所有决策都压在一个节点上,那个节点的路由空间太大了。这时候你就需要把“团队负责人”这一层加进来,也就是分层架构。
分层架构的形态通常是:顶层一个全局Supervisor,下面挂几个Team Supervisor,每个Team Supervisor再带自己的Worker团队。全局Supervisor不直接面对具体Worker,它只需要决定“这个任务属于哪个Team”,Team级别的Supervisor再决定“具体由Team内哪个Worker执行”。
这跟公司组织结构完全一致。CEO不会去管某个研发工程师今天写哪行代码,他只决定“这个项目交给研发部还是市场部”。研发部负责人再决定具体由哪个小组、哪个工程师来做。
3.2 自顶往下的任务分解流程
分层架构的核心不是“多加了一层节点”,而是“任务描述在每一层被重新结构化一次”。
全局Supervisor收到用户请求后,不是直接甩给下一个节点就完事,而是要输出一个结构化的任务分派指令。比如用户说“帮我分析一下这份数据,并生成一份汇报文档”,全局层应该把任务拆成类似这样的结构:
- 数据理解:先由数据团队负责,产出数据要点摘要。
- 文档生成:由文档团队负责,基于数据摘要生成汇报。
- 质量校验:由审核团队负责,检查上面的结果是否一致。
每一份任务分派指令需要写明目标、给定材料、期望产出、完成标准。如果这一层的任务描述是“继续下一步”这种废话,下面团队根本没法正常干活。
当Team收到上层指令后,Team Supervisor再做一次更细的拆分。数据团队内部可能还有“SQL查询”“数据可视化”“数据质量分析”三个Worker,Team Supervisor会决定先调谁、要不要串行跑多个。
执行完的结果会按相反方向逐层汇总。Worker把结论交给Team Supervisor,Team Supervisor把本团队的关键产出压缩成一份摘要交给全局Supervisor。全局Supervisor拿到的不是一个巨大的消息列表,而是几份高密度的团队报告,然后它再决定是不是还需要其他团队介入。
这个从“粗拆”到“细分”再到“摘要上行”的过程,就是分层架构在信息流上的核心逻辑。
3.3 上下文隔离与压缩策略
分层架构相比主管架构最大的红利,是你能对信息做“物理隔离”。
你可以在LangGraph里让每个Team内部使用私有状态字段。比如数据团队的所有中间查询结果都放在data_team_state下,文档团队不用看到这些字段。全局Supervisor只访问顶层的team_reports和messages。
这种隔离有多重要?我给你一组真实数据感受一下。我之前做过一个分析型系统,如果所有团队共享一个上下文,全局模型的有效上下文在第三轮之后就会被中间过程占掉一半以上。改成Team私有状态加摘要上行之后,全局模型从头到尾只看到顶层消息和每份团队报告,上下文长度稳定多了,回答质量的方差也明显下降。
压缩策略上有几点建议:
- Worker到Team的汇总,保留“结论+关键依据+异常项”,不要把完整中间过程列出来。
- Team到全局的汇总,保留“任务完成情况+核心结果+不确定项”,代码片段和详细表格不要上抛。
- 每一步压缩都不要丢“可追溯性”。最好是压缩的时候保留一个引用ID,比如“数据查询脚本ID=job_2301”,真要排查的时候可以精确找回原始过程,而不是让上层自己脑补。
3.4 分层架构的LangGraph代码骨架
用LangGraph实现分层,本质上是“图里面套图”。LangGraph支持把子图作为一个节点嵌入到父图里,这正好用来表示Team内部结构。
from langgraph.graph import StateGraph, END # ===== 数据团队的内部图 ===== def data_sql_worker(state): ... def data_chart_worker(state): ... data_team_graph = StateGraph(AgentState) data_team_graph.add_node("sql_worker", data_sql_worker) data_team_graph.add_node("chart_worker", data_chart_worker) data_team_graph.set_entry_point("sql_worker") data_team_graph.add_conditional_edges("sql_worker", decide_chart_or_end, ...) data_team_graph.add_edge("chart_worker", END) # ===== 文档团队的内部图 ===== doc_team_graph = StateGraph(AgentState) doc_team_graph.add_node("draft_worker", draft_worker) doc_team_graph.add_node("review_worker", review_worker) # ... # ===== 外层全局图 ===== workflow = StateGraph(AgentState) workflow.add_node("top_supervisor", top_supervisor_node) workflow.add_node("data_team", data_team_graph.compile()) # 嵌套子图 workflow.add_node("doc_team", doc_team_graph.compile()) # 嵌套子图 # ...用嵌套子图的方式有几个好处。
一是每个Team可以单独测试。你可以只把数据团队的子图拿过来,喂一组测试数据,验证它的路由是否正确,不需要每次都跑整个系统。
二是Team内部的状态字段天然私有。子图编译之后只暴露你定义的输入输出字段,内部加了多少“临时变量”都不会泄漏到外层。
三是如果某个Team的Worker变多,你还可以在Team内部再继续套一层,形成更深的分层结构。树的高度没有硬性限制,只要每层的路由职责足够简单。
3.5 分层的代价:调试复杂度
不要以为分层架构就是万能药。它最大的代价是调试复杂度。
主管架构下,一个任务最多只有“主管到Worker”两跳,出问题了你只要看主管的决策文本和对应Worker的返回。分层架构下,路由跳数变多,任何一个Team内部卡住,外层都不会知道具体卡在哪一步,只会看到整个子图超时。
我的习惯是给每个Team节点都加上明确的“输入输出日志”。输入要记录“从上层收到了什么任务描述”,输出要记录“返回给上层的摘要内容”。排查的时候先看顶层的任务分派是否合理,再逐层往下看每一层的输入输出是否对得上。如果某一层的输出明显偏离输入要求,问题通常就出在这一层的Team Supervisor prompt里。
还有一个横向对比的经验:分层架构适合“每个Team内部的任务类型差异很大、Team之间边界清楚”的场景。如果你拆出来的Team之间权限边界没有划分清楚,经常出现“这个任务应该由A团队还是B团队做”的争议,那问题不是出在架构上,而是出在你的业务聚类上。先回去重新画调用关系图。
4. 选型思路:主管架构还是分层架构
4.1 两者对比
拿我自己做过的几个系统来对比,主管架构和分层架构分别在下面这些维度上有明显差异:
| 对比维度 | 主管架构 | 分层架构 |
|---|---|---|
| 适用规模 | 3到10个Worker以内的系统 | 10个以上Worker,或存在多个独立业务域 |
| 路由决策点 | 集中在单个Supervisor | 分散在全局Supervisor和各Team Supervisor |
| 上下文开销 | 所有Worker返回都汇聚到顶层,上下文增长较快 | 通过Team私有状态隔离,顶层上下文更可控 |
| 调试难度 | 中等,主要看单个路由节点 | 较高,需要逐层检查路由和摘要 |
| 扩展方式 | 加Worker,但Supervisor压力线性上涨 | 加Team或加Worker,路由压力分散 |
| 典型风险 | 主管路由失焦、上下文膨胀 | 任务描述不结构化导致拆单不准、层级间信息丢失 |
单看这张表,似乎是“小用主管,大用分层”。但真实项目里更常见的是混合:全局用主管架构调度几个Team,团队内部再用一个简单的主管结构带动几个Worker,团队内部的深度不至于大到需要再套一层。我在项目里最常用的是“全局主管 + 每团队3到5个Worker”的混合形态,兼具分层的信息隔离和主管的简单直接。
4.2 实际项目里的决策路径
如果你现在手上有个新系统要设计,我给一条比较稳的决策路径。
第一步,把你所有的任务类型列出来,按领域聚类。比如“数据库操作类”“文件分析类”“生成撰写类”,每一类就是一个候选Team。
第二步,数一数每个类别下面的具体工具数量。如果每个类别下面只有两三个工具,直接用主管架构挂Worker就够了,不需要额外加一层Team,加了反而增加沟通成本。
第三步,看不同类别之间是否需要共享中间信息。如果A类任务的中间结果会被B类任务高频使用,那这两类放在同一个Team里更合适,放在不同Team就得频繁做摘要上行和下行,效率很低。
第四步,估算一下单个Supervisor的路由空间。如果顶层需要直接区分15种以上的任务类型,那就必须分层。把路由空间压到每个Supervisor只需要懂3到4个下级,这是我认为最佳的状态。
这个决策路径不复杂,但它比你照着文档把两种模式都实现一遍再对比,省力得多。
5. 常见问题与排查速查
5.1 状态被下游Worker覆盖
症状:后执行的Worker把前一个Worker写入的字段覆盖了,最终结果缺少前半段关键信息。
原因:两个Worker往同一个状态字段写入,写入方式是直接覆盖,没有使用reducer或字段级隔离。
排查方法:把每个节点对状态字段的写入操作列出来,看有没有重叠字段。尤其是messages这种列表字段,如果不用追加操作而用覆盖,一旦串行执行两个Worker,前面的结果肯定丢。
修复方式:列表字段使用追加类reducer;不同Worker的私有数据放到独立命名字段;必要的时候给状态字段加版本号或者来源标识。
5.2 路由不收敛:系统在同一个Worker之间来回循环
症状:同一个任务反复调用相同的Worker两次以上,或者Supervisor在A和B之间来回切换,直到触发递归限制。
原因:Supervisor缺少终止条件。很多情况下Supervisor会认为“再调用一次刚才的Worker可以做得更好”,但它没有从“任务是否已完成”的角度做判断。
排查方法:把最近几轮的Supervisor决策文本全部打印出来,看它每轮给的道路由理由。如果理由一直是“需要进一步优化”,说明它只关注过程,不关注完成标准。
修复方式:在Supervisor prompt里写清楚“每一类任务的完成标准是什么,达到标准必须结束”;在条件边里增加最大轮次的判断,比如超过3轮直接返回兜底结果;还可以在状态里维护一个visited_workers列表,禁止同一Worker连续被执行两次。
5.3 上下文膨胀,回答越来越差
症状:对话到第五轮之后,模型开始忽略最早的系统指令,或者回答风格漂移。
原因:中间过程信息全部塞在顶层消息里,没有压缩。尤其是检索类Worker返回的原始文本如果原样进入messages,整个上下文很快就会堆满噪音。
排查方法:统计一下每一轮结束之后的顶层状态大小。如果某个Worker执行完,状态里增加的内容占上千token,而这次结果最终只对最终答案贡献一点点信息,那就是过度暴露。
修复方式:严格执行“压缩后上行”。Worker返回前先做摘要,摘要里只留结论、关键依据、不确定项;必要时把原始数据写到外部存储,只在上行摘要里保留引用ID。
5.4 并发执行多个Worker时的写入冲突
症状:两个Worker并行执行,最终状态里只保留了一个Worker的结果。
原因:并行节点共同更新同一状态字段时发生覆盖。
排查方法:确认LangGraph的reducer是否支持合并这两个来源的更新。如果不支持,就给这两个Worker分配不同的状态字段,或者改为串行执行。
修复方式:设计状态时预留“每Worker独立字段”,并行更新时用字段隔离。状态字段的结构应该在画图阶段就设计好,不要在跑起来之后再补,容易补出一堆兼容逻辑。
5.5 模型对路由指令的遵循度不稳定
症状:主管架构下,Supervisor偶尔会把任务路由到同名相近的Worker,或者无缘无故返回结束。
原因:模型对路由决策的理解不够。尤其当两个Worker的描述里出现相似关键词,模型区分不了边界。
排查方法:把路由失误的样例收集起来,逐个对比两个Worker的工具描述是否重叠。很多时候是“职责描述写得太笼统”。
修复方式:给每个Worker增加“不在职责范围内的例子”;提升模型的推理能力级别也能缓解,但别只依赖模型升级,结构上尽量让路由表保持简单。
6. 生产化落地的三件事
6.1 会话持久化与恢复
LangGraph的checkpointer机制在开发阶段容易被忽略,但生产环境里几乎必不可少。多Agent系统的执行时间通常比单Agent长,用户可能中途关闭页面,或者某个Worker执行到一半出错误。
加了checkpointer之后,一个任务可以从最近的稳定状态继续执行,而不是重新跑一遍全流程。我习惯在每个Worker执行完就落一次checkpoint,尽量把“单步失败”的代价降到最小。
恢复逻辑要设计得贴近业务。比如报告生成到一半发现数据源更新了,合理的做法是回到数据团队那一步重新生成数据摘要,而不是从最开始的用户请求重跑。
6.2 可观测性
多Agent系统的黑盒程度比单Agent更严重,必须提前埋点。我在项目里会对每个节点输出三类日志:
- 决策日志:Supervisor或Team Supervisor收到了什么、判断了什么、为什么选这个分支。
- 结果摘要:Worker返回的压缩结果长度、关键字段、是否包含异常标记。
- 资源统计:每一步的token消耗、耗时、调用的工具名称。
把这三类日志结构化输出到追踪系统之后,排查问题会顺畅很多。否则一个任务跨三四个层,每一层又有多个节点,你根本不知道问题出在谁的决策上。
6.3 降级与人工接管
不管架构设计得多好,总有模型判断失常的时候。生产环境里不能把所有希望都押在“prompt调优”上。
我会给每个关键系统配置一条降级路径:主管架构路由连续失败N次之后,平铺成单Agent模式,用初始上下文和全部工具重新处理一次;分层架构某个Team连续报错时,把该Team的输入转给顶层手动指定一个替代流程。
人工接管则是最后一道防线。不是每个任务都要自动跑完,涉及关键数据或高风险操作时,不如直接把中间结果展示给用户,让用户决定下一步。这看起来“不够AI”,但恰恰是系统能在真实业务里站稳的基础。
最后说两个我自己项目里的体会。第一个是主管架构里Supervisor的system prompt千万别写太长。我早期总想把所有边界情况都写进去,结果路由反而变得畏手畏脚,后来砍到只剩“每个Worker擅长什么、什么情况结束”两段,准确率反而上来了。第二个是分层架构一定要给每层分别设置迭代上限,不然一个子团队可能会在一个小问题上消耗整个系统的预算。这两个坑我是线上排查到半夜才想明白的,希望你不用再踩一遍。