
1. 从一次线上事故说起单体 Agent 到底卡在哪去年年底我接手了一个内部工单系统的智能化改造需求听起来很朴素让 Agent 自动读取用户提交的问题描述判断问题类型检索知识库生成回复草稿必要时调用工单接口创建子任务。一开始我用的是最经典的单体 ReAct 结构——一个 system prompt 里塞进角色定义、工具清单、输出格式约束然后靠 Thought-Action-Observation 循环跑到底。前两周效果不错简单问题比如密码重置流程基本一次命中。但到了第三周问题开始集中爆发。最典型的一次是用户提交了一段包含三个子问题的长文本既问报销标准又问审批流卡在哪还顺带抱怨系统登录慢。单体 Agent 在第三轮循环时开始失忆——它把报销标准和审批流混在一起回答登录慢的问题直接丢了。我翻日志发现上下文已经膨胀到接近模型上限早期的 Observation 被截断模型只能靠残缺信息硬编。这不是个例。后来我陆续在几个项目里复现了同样的现象单体 Agent 的能力天花板本质上不是模型不够聪明而是上下文窗口、职责耦合和错误传播这三件事在复杂任务下同时恶化。你给它加更多工具、写更长的 prompt短期能缓解但边际收益递减得非常快。这篇文章我想把这件事讲透为什么复杂任务必然走向 Multi-Agent单体 Agent 的瓶颈具体卡在哪些环节以及如果你现在正准备从单体迁移到多体哪些坑是必须提前知道的。内容会涉及 ReAct 循环、Context 管理、Agent 拓扑与通信机制、Python 实现层面的取舍适合已经写过至少一个能跑通的 Agent、正在被复杂任务折磨的开发者。2. 单体 Agent 的三重天花板Context、职责与错误传播2.1 Context 窗口不是越大越好而是越用越脏很多人对 Context 的理解停留在窗口够大就行。现在主流模型动辄 128K、200K 甚至更高看起来绰绰有余。但实际跑起来你会发现Context 的问题不是容量而是信噪比。单体 Agent 在 ReAct 循环里每一轮都会往上下文里追加 Thought、Action、Observation。一个稍微复杂的任务跑 10 轮每轮 Observation 假设 500 token光观察结果就 5000 token。再加上工具描述、历史对话、system prompt很容易冲到几万 token。这时候模型的表现会明显下降——不是因为它读不完而是因为关键信息被淹没在大量中间过程里。我做过一个对比实验同一个知识库问答任务把历史 Observation 全量保留 vs 只保留最近 3 轮 摘要后者准确率反而高了 18%。原因很简单模型在长上下文里做注意力分配时早期的重要约束比如输出必须是 JSON会被后面的噪声稀释。提示判断你的单体 Agent 是否已经撞上 Context 天花板看两个信号——一是循环轮次超过 6 轮后输出格式开始不稳定二是同一个问题换个问法结果差异巨大。这两个都是信噪比恶化的典型症状。2.2 一个 prompt 塞进所有职责等于没有职责单体 Agent 最诱人的地方是简单一个 prompt 搞定所有事。但复杂任务天然需要多种能力——规划、检索、推理、格式化、校验。当你把这些全塞进一个 prompt模型会在不同角色之间精神分裂。举个具体例子。我之前的工单 Agent 里prompt 同时要求它严谨判断问题类型和友好地生成回复。结果模型在判断类型时过于保守因为友好语气让它倾向安抚在生成回复时又过于机械因为严谨判断的约束还在生效。这两个目标在同一个上下文里互相干扰。Multi-Agent 的核心价值之一就是把互相干扰的目标拆到不同的 Agent 里每个 Agent 的 Context 只装自己关心的信息。规划 Agent 不需要知道回复的语气回复 Agent 不需要知道检索的中间步骤。职责隔离带来的不只是清晰更是每个 Agent 的 Context 都能保持高信噪比。2.3 错误传播单体 Agent 的一错到底这是最隐蔽也最致命的问题。单体 Agent 是一个线性循环第 3 步的判断错误会直接污染第 4 步的输入而模型往往没有能力回头质疑自己。我遇到过最离谱的一次Agent 在第一步把退款误判成换货后面所有检索、回复、工单创建全部基于错误前提最后生成了一份逻辑自洽但完全跑偏的回复。整个链路没有任何一个环节能发现前提错了。Multi-Agent 里可以专门设一个校验 Agent它的唯一职责就是检查上游输出是否合理。这个校验 Agent 的 Context 里没有历史包袱它只看输入-输出这一对反而更容易发现异常。这就是用架构冗余换可靠性的思路。瓶颈维度单体 Agent 表现Multi-Agent 应对方式Context全量累积信噪比快速下降每个 Agent 独立 Context按需传递职责多目标耦合互相干扰职责隔离单 Agent 单目标错误线性传播无法自纠校验节点拦截可回溯扩展加工具即加复杂度加 Agent 即加能力边界清晰3. Multi-Agent 不是多开几个 Agent而是拓扑设计3.1 三种常见拓扑流水线、主管制、去中心很多人第一次做 Multi-Agent直觉就是多写几个 Agent 类然后串起来。但串法不同效果天差地别。我实践下来主流拓扑就三种各有适用场景。流水线式PipelineAgent A 输出给 Agent BB 给 C像工厂流水线。适合步骤明确、顺序固定的任务比如解析→检索→生成→校验。优点是简单可控缺点是任何一个环节卡住整条线都停且无法并行。主管制Supervisor一个主管 Agent 负责拆解任务、分发给下属 Agent、汇总结果。下属之间不直接通信。这是我最推荐的入门拓扑因为它最接近人类团队的工作方式调试也最容易——你只需要盯主管的决策日志。去中心式DecentralizedAgent 之间可以互相通信、协商。灵活度最高但调试难度也最高容易出现两个 Agent 互相等待或无限对话的死锁。除非任务本身需要协商比如多角色博弈否则不建议一上来就用。注意拓扑选择的第一原则是能主管制就别去中心。我见过太多团队一上来就搞去中心结果 80% 的调试时间花在排查 Agent 之间的通信死循环上。3.2 通信机制消息传递 vs 共享状态拓扑定了下一个问题是 Agent 之间怎么传数据。这里有两个流派。消息传递Agent 之间通过显式的消息对象通信每个消息包含发送者、接收者、内容、元数据。好处是边界清晰每个 Agent 只处理自己收到的消息Context 干净。坏处是需要定义消息协议前期设计成本高。共享状态所有 Agent 读写同一个全局状态对象类似黑板模式。好处是灵活任何 Agent 都能看到全局。坏处是 Context 又会膨胀而且并发读写容易出竞态。我的经验是主管制 消息传递是复杂任务下最稳的组合。主管 Agent 维护一个任务队列每个子任务打包成消息发给对应下属下属处理完把结果消息回传。这样每个下属 Agent 的 Context 里只有我收到的任务 我的工具 我的输出要求非常干净。# 一个极简的消息结构示例实际项目里我会加上 trace_id 和重试计数 from dataclasses import dataclass, field from typing import Any, Dict dataclass class AgentMessage: sender: str receiver: str task_type: str payload: Dict[str, Any] trace_id: str retry: int 0 history: list field(default_factorylist)这个结构看起来简单但trace_id和retry这两个字段是我踩坑后加的。没有 trace_id多 Agent 并发时日志根本对不上没有 retry某个 Agent 偶发失败后整条链路就断了。3.3 什么时候不该上 Multi-Agent说了这么多 Multi-Agent 的好必须泼盆冷水不是所有任务都值得多体化。判断标准很简单如果你的任务满足以下全部条件单体 Agent 就够了——步骤少于 5 步、不需要多轮工具调用、Context 稳定在 8000 token 以内、错误可以容忍。一旦有任一条件不满足才考虑多体。我见过最典型的过度设计一个只做文本分类 固定模板回复的任务硬是拆成了分类 Agent、回复 Agent、校验 Agent 三个。结果延迟翻了三倍维护成本翻了两倍效果和单体几乎一样。Multi-Agent 是解决复杂度的工具不是炫技的舞台。4. 用 Python 手写一个最小可用的 Multi-Agent 骨架4.1 为什么先手写而不是直接上框架现在 Agent 框架很多但我强烈建议至少手写一次最小骨架。原因很实在框架帮你隐藏了通信、调度、Context 管理的细节一旦出问题你根本不知道从哪查。手写一遍你会对消息怎么流转Context 怎么隔离失败怎么重试有肌肉记忆之后用框架才能用得明白。下面这个骨架我精简到最核心的部分跑起来大概 200 行但包含了主管制拓扑的完整逻辑。4.2 主管 Agent 的调度逻辑主管 Agent 的核心职责就三件事拆解任务、分发子任务、汇总结果。它自己不做具体工作所以它的 Context 里不需要工具描述只需要任务拆解规则和下属能力清单。class SupervisorAgent: def __init__(self, workers: dict, llm_client): self.workers workers # {retriever: RetrieverAgent, ...} self.llm llm_client def plan(self, user_input: str) - list: # 让模型输出一个子任务列表每个子任务指定 worker 和 payload prompt self._build_plan_prompt(user_input) raw self.llm.chat(prompt) return self._parse_plan(raw) def run(self, user_input: str) - str: subtasks self.plan(user_input) results [] for task in subtasks: worker self.workers[task[worker]] msg AgentMessage( sendersupervisor, receivertask[worker], task_typetask[type], payloadtask[payload], trace_idgen_trace_id(), ) result worker.handle(msg) results.append(result) return self._aggregate(results)这里有个关键设计主管只负责拆解和汇总不介入子任务执行。我早期版本让主管也参与执行结果它的 Context 迅速膨胀拆解质量断崖式下降。职责单一是主管 Agent 能稳定工作的前提。4.3 下属 Agent 的 Context 隔离下属 Agent 的 handle 方法里Context 只装三样东西收到的任务 payload、自己的工具描述、输出格式要求。历史对话、其他子任务的结果一律不进。class RetrieverAgent: def __init__(self, tools, llm_client): self.tools tools self.llm llm_client def handle(self, msg: AgentMessage) - dict: # Context 只包含当前任务不带任何历史 context { task: msg.payload, tools: [t.describe() for t in self.tools], output_format: json, } # 内部可以跑 ReAct 循环但循环产生的中间结果不向上传递 answer self._react_loop(context) return {trace_id: msg.trace_id, result: answer}注意_react_loop里的中间 Thought 和 Observation不向上传递只把最终结果回传。这是 Context 隔离的关键——如果每个下属都把完整循环日志回传主管的 Context 又会爆炸等于白拆。4.4 校验 Agent 的插入位置校验 Agent 放在哪直接决定它能不能拦住错误。我的经验是放在每个关键子任务的输出之后而不是整条链路最后。因为错误越早拦截修复成本越低。def run_with_validation(self, user_input): subtasks self.plan(user_input) for task in subtasks: result self.workers[task[worker]].handle(task) check self.validator.handle(result) if not check[passed]: # 带上校验反馈重试一次 task.payload[feedback] check[reason] result self.workers[task[worker]].handle(task) results.append(result) return self._aggregate(results)校验 Agent 的 prompt 要写得非常挑剔明确告诉它宁可误报也不要漏报。因为漏报的代价是错误传播到下游误报的代价只是一次重试。5. 迁移过程中最容易踩的四个坑5.1 坑一Agent 数量膨胀调度成本反超收益我第一个 Multi-Agent 项目一口气拆了 7 个 Agent。结果发现主管 Agent 的拆解 prompt 变得极其复杂——它要记住 7 个下属的能力边界还要决定任务顺序。拆解本身的错误率飙升最后效果还不如 3 个 Agent 的版本。后来我总结出一个经验值主管制下下属 Agent 控制在 3 到 5 个。超过 5 个就该考虑分层——主管下面再设一个子主管。Agent 数量不是越多越好每个 Agent 都是调度成本。5.2 坑二消息格式不统一下游解析崩溃这个坑我踩得最惨。早期每个 Agent 的输出格式都是自己定的检索 Agent 返回字符串生成 Agent 返回 dict校验 Agent 返回带嵌套的 dict。结果汇总的时候各种 KeyError。后来我强制规定所有 Agent 的输出必须是统一的 envelope 结构业务数据放在data字段里。# 统一输出格式 { trace_id: ..., status: success | failed, data: {...}, # 业务数据 error: None | ..., # 错误信息 meta: {tokens: 123, latency_ms: 456} }这个约定看起来死板但它让汇总逻辑变得极其简单也让日志分析成为可能。meta字段里的 token 和延迟数据后来成了我优化性能的主要依据。5.3 坑三无限重试导致成本失控校验失败就重试听起来合理。但如果校验 Agent 本身有 bug或者任务本身无解就会陷入无限重试。我有一次跑批处理一个任务重试了 40 多次token 账单直接爆了。必须加硬性重试上限我的默认值是 2 次。超过就标记为 failed交给人工或降级处理。同时要记录每次重试的原因方便事后分析是校验太严还是任务本身有问题。提示重试上限之外还要加一个总 token 预算。单个任务累计消耗超过预算就强制终止这是防止成本失控的最后一道闸。5.4 坑四并发下的状态污染当多个任务并发跑时如果 Agent 实例是共享的很容易出现状态污染。比如检索 Agent 缓存了上一个任务的检索结果下一个任务误用了。解决办法是每个任务用独立的 Agent 实例或者确保 Agent 内部无状态。我倾向于后者——Agent 只持有工具和 LLM 客户端这两个是只读的所有任务相关的状态都通过消息传递不留在实例里。# 无状态 Agent 的写法所有状态从 msg 里取不存实例变量 class StatelessAgent: def __init__(self, tools, llm): self.tools tools # 只读 self.llm llm # 只读 def handle(self, msg): # 所有任务状态都在 msg 里实例本身不持有任何任务数据 ...6. 从单体到多体我的迁移检查清单迁移不是重写而是渐进式重构。我现在的做法是保留单体 Agent 作为 fallback新任务走 Multi-Agent跑稳了再逐步切换。下面这份清单是我每次迁移前都会过一遍的。检查项通过标准不通过的后果任务是否真的复杂步骤 5 或需多轮工具调用过度设计成本翻倍职责能否清晰切分每个 Agent 能用一句话说清职责职责重叠互相干扰消息格式是否统一所有输出走统一 envelope汇总逻辑崩溃是否有校验节点关键子任务后有校验错误传播无法拦截重试是否有上限硬性上限 token 预算成本失控Agent 是否无状态任务状态全在消息里并发污染是否有 trace 机制全链路 trace_id 可追踪出问题无法定位这份清单里我认为最重要的是职责能否清晰切分。如果切不干净说明任务本身还没想清楚这时候硬上 Multi-Agent 只会把混乱放大。我遇到过好几次切分卡壳的时候回头重新梳理任务流程发现其实是需求本身有歧义跟架构无关。另外补充一个实操心得迁移期间一定要保留单体版本做 A/B 对比。我一般会跑一批历史任务同时用单体和多体各跑一遍对比准确率、延迟、token 消耗三个指标。只有多体在准确率上有明显优势才值得承担额外的复杂度。如果只是延迟略好那不如优化单体的 prompt。最后说个我自己的判断Multi-Agent 不是终点它只是当前模型能力下的一种工程妥协。等模型的 Context 管理和长程推理能力再上一个台阶很多现在需要多体拆分的任务未来可能单体就能搞定。所以做架构时把 Agent 之间的边界设计得清晰一点未来合并回去也容易。这是我踩了这么多坑之后最想分享的一条经验。