多Agent工程实战:主从调度、Subagent即Tool与共享记忆
最近社区里讨论度最高的一批AI公开课里清华这套“多Agent课堂”确实值得耐下心看。我最大的收获不是又多认识了几个Agent框架而是里面一个非常直白的工程观点多Agent系统真正难的不是“定义几个Agent角色”而是怎么把控制流管住让Agent在边界内协作、在需要的时候像工具一样被调用、在共享信息的时候不会互相污染。这套材料里用了大量篇幅讲两个东西主从模式下把subagent视作一种另类的tool进行调用还有多Agent共享记忆的设计边界。这两个点基本决定了一套多Agent代码是能上生产的架构还是一次性演示Demo。如果你已经能做到让单个Agent接工具、写函数、跑通一条业务流水线正准备往“多Agent编排”方向走下面这些思路应该能帮你省掉不少弯路。我不打算复述课程目录也不准备按框架API顺序讲而是把多Agent工程里最容易让人困惑的判断标准、主从调度、记忆共享、踩坑排障和落地步骤拆开聊一聊。1. 看这门课之前先想清楚一个问题多Agent是控制流设计不是模型能力设计1.1 编排失控时模型再强也救不回来我先说一个反直觉的现象。很多项目最初决定上多Agent是因为觉得单个Agent在一段很长很复杂的任务里“脑子不够用”记不住上下文、遇到分支就乱、跑几步就自己改需求。于是想当然地认为多安排几个Agent分工合作问题就解决了。但真正把多个Agent接进去之后往往会出现更头疼的现象Agent A把Agent B的上一轮指令当成了用户消息Code Agent和Review Agent互相改对方的输出来回折腾七八轮代码没变好反而越改越碎片化整个对话走廊动不动就出现“两个模型在同一个会话里激烈辩论”的场面。问题不是出在模型身上而是出在控制流上。多个Agent本质上就是多个并发但有依赖关系的计算步骤只是每一步的执行体带随机性。如果这些步骤之间没有明确的先后级、派发关系和收敛条件那它们的行为等价于几个线程不加锁地在同一块共享内存里乱写。前一个Agent输出一个中间结果后一个Agent拿它当输入继续发挥发挥完之后又把结果写回去结果偏差会像滚雪球一样叠加。强模型放进坏流程也只能把话圆得更漂亮结论依然是错的。所以从架构视角看多Agent系统要当成一个带状态机的分布式程序来设计不能当成“模型能力增强器”来看。1.2 主从模式为什么会成为默认选项公开材料里介绍多Agent编排时几乎都会给出主从结构Supervisor模式一个顶层Agent负责理解总体目标、拆解任务、派发并汇总结果底层若干个subagent各自负责一个尽量窄的领域执行完成之后把结果交还给顶层。主从能够成为多Agent设计的默认方案不是因为它在学术上最炫而是因为它让控制流始终有一个可见的决策出口。你可以介入两个subagent之间可以单独重试失败的worker可以调整某个子任务的优先级而不影响其他部分。自由协商或者全对等多Agent协作当然可以做但它的问题是出错之后你很难定位到底是哪个Agent理解错了哪个Agent在最后一步把结论带偏了没有明晰控制边界的多Agent系统线上排障会变成一场灾难。我这里说的主从并不特指某个框架的Supervisor节点。它就是一套原始但有效的调度思想给你一个顶层决策者再给你一批“随时可以被顶层调度”的执行单元。顶层决策者负责规则执行单元负责具体领域内的多步动作。1.3 动手前先用四道题筛一遍你的模块真需要拆成Agent吗结合我自己做过的好几个Agent项目我总结出一套判断标准。任何业务模块想上多Agent我都会先拿以下四句话过筛交付物是否明确。如果子任务能被定义成输出一个结构化结果比如JSON中的几个字段那适合做成独立Agent如果只能定义成“让用户聊得开心一点”先别做。拿不出客观成功标准时上层Agent无法验收worker的产出。能不能用单次LLM调用解决。如果一次普通函数调用加一个LLM就能得到满意结果那它就是一个工具不是Agent。不应该为了名词统一而强行包一层Agent。是否需要多步工具交互。把一个子模块单独做成Agent的典型理由应当是它可能需要先搜索再起草再调一个校验工具发现不合格之后自我修正反复直到满足条件。一个天然循环的过程才值得被封装成Agent。是否能在子任务级别重试。如果某个worker执行失败之后可以安全地重启只丢失该任务内部的工作内容那就适合独立拆分。如果前面所有步骤都会被这次的失败污染那引入多Agent只会成倍放大故障。按这个标准过完一遍之后你会发现能稳定产出、边界清楚、需要多轮修正的那部分任务才真正值得做成Agent。放到整个系统里看这套筛选思路也正对应了主从模式下subagent的定位它是一个需要独立持有一段状态并完成一段完整动作的执行单元而不是一个需要平等参与全局讨论的“同事”。2. 主从模式的本质subagent是另一种tool不是另一个老板多看几节课的材料之后你会发现一个反复被强调的观点最新的多Agent设计中主从模式下的subagent本质上是将子Agent作为一种另类的tool进行调用。这个说法初看有点把Agent“降维”的意思实际上它是在纠正一种常见误区。2.1 别用自然语言放养subagent要用接口契约约束它很多人刚开始设计多Agent时喜欢给每个subagent写一段“你是资深前端工程师精通想象力和创造力请和我一起完成项目”这类宽泛人设然后等着它自己发挥。这种设计在示例场景里能跑通但在真实任务里非常难控制。它缺少的是“契约”一个subagent任务进来时输入长什么样、任务完成后必须输出什么结构、失败时怎么上报都没有被约束。想让主Agent“调得动、判得准”一个subagent就得按设计普通函数的方式设计它。每个subagent需要先定义四件事入参结构除了用户任务本身之外需要携带哪些上下文、文件路径、会话标识。入参要固定成一种可序列化格式不能靠Agent猜。出参结构建议统一为状态加结果。成功时返回结构化数据失败时返回错误码和可读错误信息。最大执行步长避免worker内部陷入无限循环。每一步都消耗tokens和时间没有上限的Agent相当可怕。终止条件明确“什么时候算完成”比如输出经过校验工具、所有指定动作已执行完、或者达到任务允许的循环上限。在这些约束都定清楚以后subagent的行为就不会飘忽不定。它变成了“一段可以接受任务、内部运行多步、稳定吐出结果的受控函数”对上层而言它和工具函数在形式上确实没有本质差别。2.2 Supervisor视角每个subagent都像一个普通function call为了让“subagent即tool”更好落地可以直接在调度层做一个统一抽象。顶层Supervisor看到的工具列表里既包括普通的API查询函数、向量检索工具也包括subagent的入口。对主Agent而言它只需要做一件事根据任务意图选择一个“工具”并生成符合要求的参数。至于那个“工具”内部会不会继续调用其他模型、会不会跑好几轮检索上层并不关心。我用Python风格伪代码展示一个最朴素的实现思路WORKERS {} def register_worker(name): def decorator(fn): WORKERS[name] fn return fn return decorator register_worker(researcher) def research_worker(task: dict, memory_bus): # 内部真正跑一个带多步工具的Agent循环 steps [] for _ in range(task.get(max_steps, 5)): # 让worker模型决定查资料、写草稿还是做校验 action worker_llm_call(task, steps) steps.append(action) if action[status] done: break # 统一吐出一个结构化结果 return { status: succeeded, summary: steps[-1].get(result), logs: steps, usage: total_token_usage }Supervisor调度部分可以把这些worker全部注册成一个tools列表TOOL_SCHEMAS [ { type: function, function: { name: researcher, description: 调用研究型子Agent。当需要多轮检索、对比资料并输出总结时使用。, parameters: { properties: { task: {type: string, description: 本次调研的任务描述}, max_steps: {type: integer, description: 内部最大执行轮数} } } } }, # 其他function calling工具... ]主Agent执行时看到tools列表中有一个叫researcher的函数它判断当前任务适合调用这个函数于是生成一段tool_call参数。真正执行这个tool_call的代码在内部启动一个独立的subagent会话给这个worker注入它自己的专属提示词、专属工具栈、专属记忆片段。worker内部的模型自己决定要搜索几次、要不要调整输出直到达到出口条件后把精简后的结果返回给主模型。对主模型来说subagent和fetch_score之类工具的差别只是执行时间更长、输出更复杂、可能有更多内部副作用。这正是把它当tool看、而不是当“平级协商者”看的合理之处。2.3 subagent什么时候应该“降级”成普通函数调用如果你认同“subagent是另一种tool”那么反向操作就是如果一个模块的复杂度不够格当Agent就别让它硬套Agent壳。举个实际例子。有人会把一个“意图分类器”做成subagent理由是分类器内部也要调用一次模型解释一下用户语义。但在我的实施经验里这种场景更适合直接暴露成function calling或者普通LLM处理函数。意图分类通常是短任务一次模型调用就能完成中间不需要状态、不需要多步循环强行包一层subagent只会增加一层tool调度时间多了一个可能失败的节点。什么情况下才值得把某个模块升级为真正的subagent我的标准是这个子任务需要独立持有上下文并自主决策多轮动作且动作之间存在依赖关系完成后必须产出一个可以被上层校验的结果。如果一个模块只是“调一次模型根据输出选择分支”那它就是普通函数只有当一个模块需要“先取数据再完成草稿再用脚本校验不过就改直到输出能通过检查”时才需要Agent化。把普通函数当工具管把具备内部循环能力的Agent也当工具管可以让整个系统保持一致的调用范式这个思路在复杂业务里尤其有价值。3. 多Agent共享记忆共享的从来不是“同一份聊天记录”网络热词里好几次提到“多Agent共享记忆”。不少初学者会把它理解成既然Agent之间要协作那就应该让每个Agent都看到对话历史的全部内容这样大家信息才一致。实践一遍你就会发现这基本是在制造灾难。上下文成倍膨胀、Agent互相看到无关信息、前序任务里的污染内容被后续任务当成了新事实问题远比“记忆太少”更严重。共享记忆的关键不是“要不要共享”而是共享什么、让谁看见、保留多久。我把需要共享的内容先分成三类任务上下文当前用户正在做什么、输入是什么、目标是什么。这部分通常在会话窗口里维护适合给执行型subagent传递窄切片。中间产物Agent A产生的结构化结果Agent B需要在此基础上继续加工。这种情况不应该靠“共同读聊天记录”来传递而应该作为明确的入参由主Agent在调度时传给下一个worker。长期知识过去项目沉淀下来的模式、用户偏好、跨session的历史结论。这部分一般放到外部记忆库而不是塞进Agent上下文。在分析完共享内容后再选择工程实现你会有一种“豁然开朗”的感觉。3.1 三种主流记忆共享方案共享窗口、向量记忆库、事件日志围绕上述三类内容目前最常见的实现有三种。共享工作区也就是一块所有人都能读写的文本区域所有Agent把上下文历史、中间结论、待办事项写在同一份数据里。优势是实现非常简单只要一个全局变量或一份消息列表就能完成信息一致性很高适合流程比较短、Agent数量少的Demo。坏处也明显随着任务推进令牌会快速膨胀所有Agent都被迫把越来越多的无关历史当成上下文接收两个Agent同时写同一块区域还容易互相覆盖。我建议只把共享窗口用在两到三个Agent、任务总步骤不超过十几步的小闭环里。向量记忆库适合承载长期知识类内容。Agent需要在任务开始时检索知识片段在任务结束后把新发现的关键信息写入向量库。它的优势是能支持超长历史跨session经验共享效果好Agent只加载与当前任务相关的记忆片段。缺点是需要额外维护一套索引记忆检索本身不一定稳定——有可能召回一些噪声数据从而降低后续决策质量。事件日志/消息总线让所有Agent之间的协作都以结构化事件的形式沉淀下来像流水账一样记录完整执行链路。每个Agent按主题订阅自己关心的事件主Agent可以看到全局事件流。这种实现的生产优势非常独特数据可追溯出错可以重放测试时可以拿一段完整事件序列做回归。缺点是需要设计清晰的事件Schema还要有配套的基础设施不适合小项目开箱即用。三种方案的定位差异可以用这张表快速对照实现方式最大优势最大短板推荐场景共享工作区/共享窗口实现简单上下文完全一致token膨胀快互相干扰明显小规模、流程短的Demo向量记忆库可扩展支持长期记忆检索有噪声需维护索引知识复用、跨session场景事件日志/消息总线可追溯可重放利于观测需要额外基础设施与Schema设计追求稳定性的生产级系统3.2 由谁来写记忆比记忆放哪里更重要写共享记忆时最忌讳的是“每个Agent都能写”。当所有worker都可以随时向共享状态里添加自己的结论时你实际上制造了一个没有锁的并发写环境。两个Agent各自基于不同阶段的信息产生结论又都把它们写入同一个全局状态后写的人覆盖先写的人系统就开始出现无法解释的“记忆漂移”。在我自己的设计里会刻意把共享记忆的写入权收拢。执行型subagent产生的中间结论先返回给顶层Agent由顶层决定哪些信息值得持久化哪些只是临时推理产物哪些需要废弃。写入操作的入口尽量收敛
