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 类,然后串起来"。但串法不同,效果天差地别。我实践下来,主流拓扑就三种,各有适用场景。
流水线式(Pipeline):Agent A 输出给 Agent B,B 给 C,像工厂流水线。适合步骤明确、顺序固定的任务,比如"解析→检索→生成→校验"。优点是简单可控,缺点是任何一个环节卡住整条线都停,且无法并行。
主管制(Supervisor):一个主管 Agent 负责拆解任务、分发给下属 Agent、汇总结果。下属之间不直接通信。这是我最推荐的入门拓扑,因为它最接近人类团队的工作方式,调试也最容易——你只需要盯主管的决策日志。
去中心式(Decentralized):Agent 之间可以互相通信、协商。灵活度最高,但调试难度也最高,容易出现"两个 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_factory=list)这个结构看起来简单,但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( sender="supervisor", receiver=task["worker"], task_type=task["type"], payload=task["payload"], trace_id=gen_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 之间的边界设计得清晰一点,未来合并回去也容易。这是我踩了这么多坑之后,最想分享的一条经验。