1. 从一次多智能体任务失控说起去年年底我接手了一个内部工具链的改造需求核心场景是让一个主控智能体去调度若干个子智能体分别处理代码检索、文档摘要、接口校验、日志分析这几类活。一开始我想得很简单主智能体拿到用户请求判断意图然后把子任务分发给对应的子智能体等它们返回结果再拼装。听起来像是一个标准的编排流程写起来也不复杂。结果第一次跑真实任务就翻车了。四个子智能体同时启动其中两个在访问同一份共享的会话状态互相覆盖对方的中间变量另一个因为外部接口响应慢卡了将近四十秒主智能体一直在等它整个链路被拖死还有一个子智能体在重试逻辑里陷入了循环反复调用同一个工具把配额烧掉了一大半。那次事故之后我才真正意识到子 Agent 编排不是把任务分出去再收回来这么简单它本质上是一个分布式任务调度问题只不过执行单元从进程变成了智能体。这篇内容我想聊的就是我在这个过程中沉淀下来的一套编排思路独立上下文、并发执行、超时检测。这三个词看起来平平无奇但每一个背后都有具体的坑和取舍。如果你正在做多智能体系统或者准备在现有工作流里引入子智能体这些经验应该能帮你少走一些弯路。整套思路不依赖特定框架无论你用的是哪家的 Agent 框架核心逻辑都是通用的。2. 为什么子 Agent 必须拥有独立上下文2.1 共享上下文是多智能体系统里最隐蔽的炸弹很多人第一次设计多智能体系统时会本能地让所有子智能体共享主智能体的上下文。理由很直观信息都在一个池子里子智能体不用重复获取背景主智能体也能随时看到每个子任务的状态。这个设计在小规模、串行执行的场景下确实没问题但一旦进入并发场景它就会变成一颗定时炸弹。我遇到的最典型的问题是这样的主智能体维护一个messages列表里面包含系统提示、用户输入、历史对话。子智能体 A 负责检索代码它往列表里追加了一条工具调用结果子智能体 B 负责摘要文档它也往同一个列表里追加内容。两个子智能体并发执行时追加顺序是不确定的而且它们各自看到的历史是不一样的——A 可能看到了 B 还没写完的半截内容B 可能基于 A 的中间状态做出了错误判断。这种问题不会报错只会让结果变得莫名其妙排查起来极其痛苦。更麻烦的是上下文污染。子智能体在执行过程中会产生大量的中间推理、工具调用记录、失败重试信息。这些东西如果全部回写到主上下文主智能体的上下文窗口会被迅速撑爆而且里面充斥着大量与主任务无关的噪声。主智能体在做下一步决策时会被这些噪声干扰判断质量明显下降。2.2 独立上下文的具体设计每个子 Agent 一个沙箱我的做法是给每个子智能体分配一个完全独立的上下文空间我把它叫做上下文沙箱。具体来说主智能体在派发任务时只传递三样东西给子智能体任务指令这个子智能体要做什么用自然语言描述清楚包含必要的约束条件。最小必要输入完成任务所必需的数据比如待检索的代码片段、待摘要的文档内容、待校验的接口定义。注意是最小必要不是全部相关。输出格式约定子智能体应该以什么结构返回结果比如 JSON schema 或者固定的字段列表。子智能体在自己的沙箱里可以自由地推理、调用工具、重试这些过程全部留在沙箱内部不会外泄。它最终只把结构化的结果返回给主智能体。主智能体拿到结果后决定是继续派发下一个任务还是汇总输出。这个设计带来的好处是显而易见的。首先是隔离性子智能体之间互不干扰A 的中间状态不会影响 B 的判断。其次是可控性主智能体的上下文始终保持干净只包含任务指令和结果窗口利用率高。最后是可调试性每个子智能体的完整执行轨迹都留在自己的沙箱里出问题时可以单独回放不用在一大堆混杂的日志里捞针。2.3 上下文传递的边界什么该传什么不该传独立上下文的关键难点在于边界划分。传少了子智能体信息不足做出来的结果没法用传多了又退回到共享上下文的老路。我总结了一个判断标准只传子智能体无法自行获取、且完成任务必需的信息。举个例子。主智能体要派一个子智能体去分析某段代码的性能瓶颈。代码本身必须传因为子智能体没法自己拿到。但代码所在项目的整体架构、历史提交记录、团队编码规范这些如果子智能体可以通过工具自行检索就不要塞进初始上下文。让它自己去查一方面减少初始负载另一方面也避免了信息过时的问题。还有一个容易被忽略的点不要把主智能体的推理过程传给子智能体。有些实现为了让子智能体理解意图会把主智能体的思考链一起传过去。这是有害的。主智能体的推理可能包含错误的假设、未验证的猜测子智能体拿到这些会先入为主反而限制了它的独立判断。正确的做法是只传结论性的任务描述让子智能体从干净的状态开始。提示独立上下文不等于完全隔离。子智能体之间如果需要协作应该通过主智能体中转而不是直接共享状态。主智能体充当协调者的角色这样所有跨智能体的数据流动都是显式的、可追踪的。3. 并发执行让子 Agent 真正跑起来3.1 串行编排的天花板在哪里最开始我的编排逻辑是串行的主智能体派发任务 A等 A 返回再派发任务 B等 B 返回以此类推。这个模式写起来最简单调试也最直观但性能天花板非常低。假设一个用户请求需要拆成五个子任务每个子任务平均耗时三秒串行执行就是十五秒。用户等十五秒才看到结果体验很差。更糟糕的是这五个子任务里可能只有两个是真正耗时的另外三个是轻量的判断类任务本来可以瞬间完成却因为串行排队被拖慢了。我做过一个粗略的统计在一个典型的多智能体工作流里串行执行的总耗时约等于所有子任务耗时之和而并发执行的总耗时约等于最慢的那个子任务耗时。当子任务数量增加时这个差距会急剧放大。五个子任务时是五倍差距十个子任务时就是十倍。对于交互式应用来说这个差距直接决定了产品能不能用。3.2 并发编排的实现任务图与依赖解析并发不是简单地把所有子任务同时扔出去。子任务之间往往存在依赖关系任务 C 需要任务 A 和任务 B 的结果作为输入那 C 就必须等 A 和 B 都完成才能启动。所以并发编排的核心是构建任务依赖图然后按拓扑顺序分层执行。我的实现方式是这样的。主智能体在规划阶段产出一个任务列表每个任务包含任务 ID、任务描述、依赖的任务 ID 列表。然后编排器做一次依赖解析把没有依赖的任务放在第一层依赖第一层任务的结果放在第二层以此类推。每一层内部的任务可以完全并发执行层与层之间是串行等待。举个具体的例子。一个代码审查请求可能拆成这样的任务图任务 ID任务描述依赖T1检索目标代码文件无T2检索相关测试用例无T3静态分析代码质量T1T4检查测试覆盖率T1, T2T5汇总审查报告T3, T4执行时T1 和 T2 并发启动。两者都完成后T3 和 T4 并发启动。T3 和 T4 都完成后T5 启动。整个流程的耗时是 max(T1, T2) max(T3, T4) T5而不是五个任务耗时之和。这个模式的关键在于依赖声明的准确性。如果依赖声明得太保守比如把所有任务都标成依赖前一个任务那就退化成了串行。如果依赖声明得太激进比如漏掉了某个真实依赖就会出现子任务拿到空数据或者错误数据的情况。我的经验是宁可稍微保守一点也不要漏依赖因为漏依赖导致的错误往往很隐蔽而保守一点只是损失一些并发度。3.3 并发度控制不是越多越好有了并发能力之后很容易产生一个冲动把所有能并发的任务全部并发出去。我一开始就是这么干的结果踩了不少坑。第一个坑是资源竞争。子智能体在执行时往往需要调用外部工具比如数据库查询、API 请求、文件读写。这些资源都有并发上限。如果同时启动二十个子智能体每个都去查数据库数据库连接池瞬间被打满后面的请求全部排队或者超时。我遇到过最夸张的一次三十个子智能体同时调用同一个内部 API直接把那个服务打挂了。第二个坑是成本失控。如果子智能体背后是大模型调用每个子智能体都要消耗 token。并发数太高时短时间内会产生大量并发请求不仅费用飙升还可能触发服务商的速率限制导致部分请求被拒绝。所以并发度必须控制。我的做法是设置一个并发窗口比如同时最多运行五个子智能体。编排器维护一个任务队列每当有一个子智能体完成就从队列里取出下一个可执行的任务启动。这样既保证了并发效率又不会把资源打爆。并发窗口的大小需要根据实际情况调整。如果子智能体主要是做本地计算窗口可以大一些比如十到二十。如果子智能体需要调用外部服务窗口就要小一些通常三到五比较稳妥。如果外部服务有明确的速率限制窗口大小应该根据限制反推比如限制是每秒十次请求每个子智能体平均发起两次请求那窗口就不应该超过五。注意并发窗口不是固定值最好做成可配置的并且支持动态调整。在系统负载高的时候自动缩小窗口负载低的时候放大窗口这样能在稳定性和效率之间取得平衡。4. 超时检测别让一个子 Agent 拖垮整个链路4.1 子 Agent 为什么会卡住子智能体卡住的原因比普通函数调用复杂得多。普通函数要么返回要么抛异常行为是可预测的。子智能体的执行路径是模型驱动的它可能在任何一个环节陷入困境。我遇到过的情况包括子智能体在推理时反复纠结同一个问题生成了大量无意义的思考内容迟迟不输出最终结果子智能体调用的外部工具响应极慢它一直在等待没有设置自己的超时子智能体在重试逻辑里打转每次失败都重新尝试但失败原因是根本性的重试多少次都没用还有一次是子智能体生成的输出格式不符合约定解析器反复尝试解析失败进入了死循环。这些情况有一个共同点子智能体自己不会主动放弃。它会一直尝试下去直到外部强制中断。如果没有超时检测一个卡住的子智能体就会一直占用并发窗口的位置导致后续任务无法启动整个编排链路被拖死。4.2 分层超时设计从粗到细的三道防线我的超时检测是分层的一共三道防线每一道负责不同的粒度。第一道是子智能体级别的总超时。每个子智能体在启动时都会带一个最大执行时间比如六十秒。超过这个时间无论它在做什么都强制终止返回一个超时错误。这是最粗粒度的保护确保没有任何子智能体能无限期运行。第二道是单步操作级别的超时。子智能体在执行过程中会有多个步骤比如一次模型调用、一次工具调用、一次结果解析。每个步骤都有自己的超时。模型调用可能设置三十秒工具调用根据工具类型设置五到十五秒结果解析设置两秒。这样即使总超时还没到某个卡住的步骤也会被及时发现并中断。第三道是心跳检测。子智能体在执行过程中会定期发送心跳信号表示自己还活着。如果编排器在预期时间内没有收到心跳就判定该子智能体已经失去响应主动终止它。这道防线主要针对那些看起来还在运行但实际上已经卡死的情况比如模型调用返回了但子智能体在后续处理中陷入了死循环。这三道防线的关系是层层递进的。正常情况下子智能体会在总超时之前完成单步超时和心跳检测都不会触发。当某个步骤变慢时单步超时会先触发给子智能体一个纠正的机会。如果子智能体整体变慢总超时会兜底。如果子智能体完全失去响应心跳检测会兜底。4.3 超时后的处理重试、降级还是放弃超时检测只是第一步超时之后怎么处理才是关键。我的策略是根据任务的重要性和可替代性来决定。对于关键路径上的任务比如主智能体必须拿到结果才能继续的任务超时后我会尝试一次重试。重试时会做一些调整如果上次是因为模型调用超时这次可以换一个更快的模型或者简化提示如果上次是因为工具调用超时这次可以换一个备用工具或者调整参数。重试只做一次如果还超时就返回失败让主智能体决定怎么处理。对于非关键路径上的任务比如锦上添花的分析类任务超时后直接放弃返回一个空结果或者默认值。主智能体在汇总时会跳过这些任务不影响整体流程。对于可降级的任务比如一个复杂的分析任务超时后可以降级成一个简单的版本。比如原本要分析整个代码库的性能超时后只分析核心模块。这样虽然结果不够全面但至少能给出一些有价值的信息。这里有一个重要的原则超时处理必须是显式的不能静默失败。子智能体超时后编排器要记录详细的日志包括超时发生在哪个步骤、当时的上下文是什么、已经完成了多少工作。这些信息对于后续优化超时阈值、改进子智能体设计都非常有价值。5. 把三件事串起来一个完整的编排流程5.1 从用户请求到任务图整个编排流程从主智能体接收用户请求开始。主智能体首先做意图理解判断这个请求需要拆成哪些子任务。这一步的输出是一个任务列表每个任务包含描述、依赖关系、优先级、超时配置。任务拆分的粒度需要仔细把握。拆得太粗子智能体承担的工作太多容易超时而且失败后重试成本高。拆得太细子智能体数量太多编排开销和上下文传递开销都会增加。我的经验是每个子智能体承担的工作量应该控制在它能在三十到六十秒内完成的范围内。如果一个子任务预计需要更长时间就继续往下拆。任务拆分完成后编排器做依赖解析构建任务图计算每个任务的执行层级。同时编排器会为每个任务分配一个独立的上下文沙箱准备好最小必要的输入数据。5.2 并发调度与超时监控的协同任务图构建完成后编排器启动调度循环。调度循环的核心逻辑是维护一个就绪队列里面是所有依赖已满足且尚未启动的任务维护一个运行集合里面是当前正在执行的子智能体维护一个并发窗口控制运行集合的大小。每一轮调度编排器从就绪队列里取出任务直到运行集合达到并发窗口上限。每个启动的子智能体都会带上自己的超时配置和心跳间隔。编排器会启动一个监控协程负责检查每个运行中的子智能体是否超时、心跳是否正常。当子智能体完成时编排器把它的结果写入对应的上下文沙箱标记该任务完成然后检查是否有新的任务因为依赖满足而进入就绪队列。当子智能体超时时编排器根据预设策略决定重试、降级还是放弃然后释放并发窗口的位置。这个循环一直持续到所有任务完成或者失败。最后主智能体收集所有子智能体的结果做最终的汇总和输出。5.3 一个真实的执行时序为了让你更直观地理解我描述一个真实的执行时序。假设用户请求是审查这个项目的代码质量并给出改进建议主智能体拆出了五个任务依赖关系如前面表格所示。时间零点编排器启动 T1 和 T2并发窗口占用两个位置。T1 检索代码文件T2 检索测试用例。两者都在三秒内完成结果写入各自的沙箱。时间三秒T1 和 T2 完成T3 和 T4 的依赖满足编排器启动它们。T3 做静态分析T4 检查测试覆盖率。T3 耗时八秒T4 耗时十二秒。时间十五秒T3 和 T4 都完成T5 的依赖满足编排器启动 T5。T5 汇总审查报告耗时五秒。时间二十秒T5 完成所有任务结束。主智能体收集 T5 的结果格式化成最终输出返回给用户。整个流程耗时二十秒。如果串行执行耗时会是三加三加八加十二加五共三十一秒。并发带来的提升接近百分之四十。而且这还是在子任务数量较少的情况下如果子任务更多提升会更明显。6. 踩过的坑与对应的解法6.1 上下文沙箱的内存泄漏独立上下文虽然好但如果管理不当会造成内存泄漏。每个子智能体的沙箱里会积累大量的中间数据包括模型调用的完整请求和响应、工具调用的输入输出、推理过程的每一步。如果这些沙箱在执行完成后没有及时释放内存占用会持续增长。我遇到过一次线上问题服务运行了几天之后内存告警。排查发现是子智能体沙箱没有释放每个沙箱平均占用几兆内存累积了几千个沙箱直接把内存吃满了。解法很简单子智能体完成后只保留最终结果和必要的执行摘要其余中间数据全部释放。如果需要保留完整轨迹用于调试就写入磁盘或者日志系统不要留在内存里。6.2 并发窗口与外部限流的冲突前面提到并发窗口要根据外部服务的限流来设置但实际操作中外部服务的限流往往是动态的。比如某个 API 在高峰期限流更严格在低峰期限流宽松。如果并发窗口是固定值高峰期就会频繁触发限流低峰期又浪费了并发能力。我的解法是引入一个自适应并发控制机制。编排器监控子智能体的失败率如果失败率上升且失败原因是限流就自动缩小并发窗口如果失败率下降且系统负载不高就逐步放大并发窗口。这个机制不需要很复杂一个简单的比例控制器就能工作得很好。6.3 超时阈值的设定难题超时阈值设多少合适这个问题没有标准答案。设得太短正常的子智能体会被误杀导致任务失败率上升。设得太长卡住的子智能体会占用资源太久影响整体吞吐。我的做法是基于历史数据动态调整。编排器记录每个类型子智能体的执行耗时分布计算出 P50、P95、P99 分位数。超时阈值设定在 P99 的一点五倍左右。这样既能覆盖绝大多数正常情况又能在真正卡住时及时中断。当然这个阈值需要定期重新计算因为子智能体的行为会随着模型更新、工具变化而改变。6.4 子智能体之间的结果冲突即使有独立上下文子智能体之间的结果仍然可能冲突。比如两个子智能体都对同一段代码给出了改进建议但建议互相矛盾。主智能体在汇总时需要处理这种冲突。我的处理方式是让主智能体做冲突检测和仲裁。主智能体在收集结果时会检查不同子智能体的输出是否存在矛盾。如果存在主智能体会根据任务优先级、子智能体的置信度、以及自身的判断来决定采纳哪个或者在最终输出中同时呈现两种观点并说明分歧。这个机制不需要很复杂但必须有否则最终输出的质量会受影响。7. 一些可以立刻用上的实操建议如果你准备在自己的系统里实现子 Agent 编排下面这些建议可以直接参考。关于独立上下文从第一天就做好隔离不要等到出问题再改。沙箱的创建和销毁要成对出现确保不会泄漏。传递数据时坚持最小必要原则宁可让子智能体多查一次也不要塞一堆可能用不上的信息。关于并发先实现串行版本确保逻辑正确再改造成并发。并发改造时先固定并发窗口为一然后逐步放大观察系统表现。依赖声明要仔细审查最好有自动化检查确保没有循环依赖和遗漏依赖。关于超时三道防线都要有缺一不可。超时后的处理策略要提前定义好不要等到超时了再临时决定。超时日志要详细这是后续优化的基础。关于监控编排器的每一个决策都要有日志包括任务拆分、依赖解析、调度决策、超时处理、结果汇总。这些日志在排查问题时价值极高。同时要监控关键指标任务成功率、平均耗时、超时率、并发窗口利用率、外部服务失败率。关于测试多智能体系统的测试比单智能体复杂得多。除了正常路径要重点测试异常路径子智能体超时、子智能体返回错误格式、外部服务不可用、依赖解析失败、并发窗口打满。这些场景在真实环境中一定会出现提前测过才能从容应对。我在实际使用中发现子 Agent 编排的复杂度主要不在于单个子智能体的实现而在于它们之间的协调。独立上下文解决了状态隔离问题并发解决了效率问题超时检测解决了稳定性问题。这三件事做好了整个系统的可靠性会有质的提升。至于具体的框架选型、模型选择、提示词设计那些都是可以逐步迭代的但编排的骨架必须一开始就搭对。