ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从单体到Multi-Agent:复杂任务为何必然走向多智能体架构

从单体到Multi-Agent:复杂任务为何必然走向多智能体架构

1. 从一次上下文溢出报错说起:单体 Agent 的真实瓶颈

如果你正在做 Agent 开发,大概率见过这类报错:API error: 400 this model's maximum context length is 1048576 tokens,或者更隐蔽的error during compaction。很多人第一反应是"模型上下文不够大",于是换更大的模型、加更狠的压缩策略,结果发现任务成功率并没有本质提升。问题不在窗口大小,而在于单体 Agent 把"思考、记忆、执行、校验"全部塞进一条上下文流里,这条流迟早会被撑爆,而且撑爆的方式往往不是硬性报错,而是模型开始"忘事""跑偏""自我重复"。

我最早做 Agent 项目时也是单体派。一个 ReAct 循环,一个 system prompt,一堆工具定义,跑简单的信息检索、单文件代码修改、格式化输出,确实够用。但只要任务链条一长——比如"读三个仓库的代码,对比接口差异,生成迁移方案,再改五个文件并跑测试"——单体 Agent 就开始露馅。它会在第 8 步忘了第 2 步读到的关键约束,会在工具返回超长日志后把前面的计划覆盖掉,会在自我校验时既当运动员又当裁判,最后给你一个"看起来完成了"但根本跑不通的结果。

这篇文章想聊的不是"Multi-Agent 有多先进"这种空话,而是为什么复杂任务在工程上必然走向 Multi-Agent,以及这个"必然"背后的具体机制:上下文为什么会失控、职责分离为什么能救场、编排层到底在解决什么问题、什么时候你其实不需要 Multi-Agent。我会结合 ReAct、Context 管理、Python 实现、Agent 框架与编排这些关键词,把单体天花板拆开给你看,再给出可落地的多智能体拆分思路。

适合谁看:已经写过单体 Agent、踩过上下文和稳定性坑、正在纠结要不要上 Multi-Agent 的开发者;也适合刚入门 Agent 开发、想少走弯路的人。全文不堆概念,重点讲"为什么"和"怎么做"。

2. 单体 Agent 的上下文为什么会失控:三个绕不开的机制

2.1 ReAct 循环的上下文是单调增长的

ReAct 的核心是"Thought → Action → Observation"循环。每轮循环都会往上下文里追加:模型的思考、工具调用的参数、工具返回的结果。关键在于,这些内容只增不减。你可能会说,可以做历史压缩、可以做摘要。但压缩本身是有损的,而且压缩时机很难拿捏。

我做过一个实测:让单体 Agent 处理一个"扫描 20 个 Python 文件、找出所有未处理的异常、生成修复补丁"的任务。前 5 个文件时,模型表现很好,思考清晰、补丁准确。到第 12 个文件左右,工具返回的代码片段累计已经占了大半上下文,模型开始出现两种典型退化:一是把前面文件的修复模式错误地套用到当前文件,二是"忘记"了最初设定的补丁格式要求,输出风格开始漂移。这不是模型笨,是有效注意力被稀释了。

提示:上下文长度不等于有效上下文。模型能"看到"100 万 token,不代表它能对其中每一段保持同等注意力。长上下文里的信息衰减是真实存在的。

2.2 工具返回结果是上下文的"污染源"

单体 Agent 里,所有工具的返回值都直接进同一条上下文。一个grep返回 3000 行日志,一个read_file返回整个文件,一次run_tests返回几百行失败堆栈——这些都会永久占据上下文预算。更麻烦的是,这些原始数据里 90% 是噪声,但模型没法主动"扔掉",它只能被动接收。

我见过最夸张的案例:一个 Agent 调用数据库查询工具,返回了 2 万行 JSON,直接把上下文顶到上限,触发maximum context length报错,整个任务中断。后来我们改成让工具侧先做聚合,只返回统计摘要,问题立刻缓解。这说明什么?上下文管理不该只靠模型,工具层和编排层必须参与。而单体架构下,工具和"大脑"是耦合的,你很难在中间插入一个"数据清洗层"。

2.3 单一职责的 prompt 无法同时满足多种角色

单体 Agent 通常用一个 system prompt 承担所有职责:既要它严谨规划,又要它大胆执行,还要它客观校验。这三者在 prompt 层面是互相冲突的。规划需要保守、考虑边界;执行需要果断、少犹豫;校验需要怀疑、挑毛病。你把它们塞进一个 prompt,模型只能在中间取一个折中值,结果就是"规划不够细、执行不够狠、校验不够严"。

这就像让一个人同时当项目经理、施工员和质检员。他当然能干活,但每个角色都做不到位。Multi-Agent 的本质,就是把这三个角色拆成三个独立的上下文和 prompt,让每个 Agent 只对自己的职责负责。这不是为了炫技,而是为了让每个 prompt 都能调到最优。

3. 职责分离为什么能突破天花板:从"一个大脑"到"一条流水线"

3.1 每个 Agent 拥有独立的上下文预算

Multi-Agent 最直接的收益是上下文隔离。规划 Agent 的上下文里只有任务目标和约束,没有工具返回的原始日志;执行 Agent 的上下文里只有当前子任务和必要的输入,没有全局历史;校验 Agent 的上下文里只有产出物和验收标准,没有执行过程的噪声。每个 Agent 的上下文都短、干净、聚焦,模型的有效注意力自然就上去了。

我做过对比:同一个"代码迁移"任务,单体 Agent 成功率大概 40%,经常在第 6 到第 10 步之间崩掉;拆成"规划 + 执行 + 校验"三个 Agent 后,成功率提到 75% 以上。提升不是来自模型变强,而是每个环节的上下文都变干净了。执行 Agent 不需要知道全局计划,它只需要知道"现在改哪个文件、改成什么样"。

3.2 角色化 prompt 让每个环节都能调到最优

拆开之后,你可以给每个 Agent 写专门的 prompt。规划 Agent 的 prompt 可以强调"列出所有依赖、标注风险点、给出回滚方案";执行 Agent 的 prompt 可以强调"严格按给定步骤操作、遇到不确定就停下报告";校验 Agent 的 prompt 可以强调"默认产出物有问题、逐条对照验收标准、给出具体反例"。

这种角色化带来的效果,比单纯换模型明显得多。我试过用同一个中等模型,只改 prompt 角色,校验 Agent 挑出的问题数量翻了一倍。原因很简单:当 prompt 只服务一个目标时,模型不需要在多个目标之间做权衡,它的输出会更极端、更专业。

3.3 失败可以被局部化,而不是全局重来

单体 Agent 最让人崩溃的是"一步错、步步错"。第 3 步读错了一个配置,到第 15 步才发现,前面 12 步全部白做。Multi-Agent 里,每个子任务是一个独立的执行单元,失败了只需要重跑那一个单元,不需要推翻全局。

这里有个关键设计:子任务的输入输出必须是显式的、可序列化的。比如执行 Agent 的输入是{file_path, target_pattern, constraints},输出是{patch, status, notes}。这样校验 Agent 可以独立检查,编排层也可以根据 status 决定重试还是升级。这种"接口化"的拆分,是 Multi-Agent 能稳定运行的前提。

4. 编排层到底在解决什么:不是调度,是状态与契约

4.1 编排的核心是维护一份"可信状态"

很多人以为 Multi-Agent 的难点是"怎么让 Agent 之间通信",其实真正的难点是维护一份全局可信状态。规划 Agent 产出的计划、执行 Agent 产出的中间结果、校验 Agent 产出的反馈,这些都需要一个地方统一管理。这个"地方"就是编排层。

编排层要回答几个问题:当前任务进行到哪一步?哪些子任务已完成、哪些失败、哪些待执行?某个子任务的输入依赖哪些前置产出?如果某个 Agent 返回了不符合契约的结果,是重试、降级还是终止?这些问题在单体 Agent 里是不存在的,因为单体只有一个上下文,状态隐含在上下文里。一旦拆开,状态就必须显式化。

我用 Python 实现过一个轻量编排器,核心就是一个状态字典加一个任务队列。状态字典记录每个子任务的status、input、output、retry_count;任务队列按依赖顺序调度。听起来简单,但把状态显式化之后,整个系统的可观测性和可调试性提升了一个量级。出问题时你能直接看到是哪个子任务、哪次重试、哪个字段出了问题,而不是对着一坨上下文猜。

4.2 Agent 之间的"契约"比通信协议更重要

Multi-Agent 里,Agent 之间传递的不是自然语言闲聊,而是结构化契约。规划 Agent 输出的计划必须是可解析的(比如 JSON 数组,每项包含task_id、description、depends_on、acceptance_criteria);执行 Agent 的输出必须包含status和artifact;校验 Agent 的输出必须包含passed和issues。

为什么强调契约?因为自然语言在 Agent 之间传递会引入歧义和漂移。A Agent 说"把配置改一下",B Agent 可能理解成改文件名,也可能理解成改内容。契约化之后,字段名和取值都是确定的,歧义空间被压到最小。这也是为什么很多成熟的 Agent 框架都在推"结构化输出"——不是为了好看,是为了让多 Agent 协作可预测。

4.3 编排不是越复杂越好:三种常见模式

实际项目里,编排模式不用一上来就搞得很复杂。我总结下来常用的就三种:

模式适用场景特点
流水线式步骤固定、依赖线性实现简单,规划 Agent 一次产出全流程
主管-工人式子任务可并行、数量不定主管 Agent 动态派发,工人 Agent 独立执行
辩论/评审式需要高质量决策多个 Agent 给方案,评审 Agent 择优或融合

大部分"复杂任务"用流水线式加一个校验环节就够了。主管-工人式适合那种"扫描 N 个目标、每个目标独立处理"的场景。辩论式成本高,一般只在关键决策点用。选编排模式的标准是任务结构,不是技术炫技。

5. 什么时候你其实不需要 Multi-Agent:避免过度设计

5.1 任务短、工具少、上下文可控时,单体更划算

Multi-Agent 不是免费的。它带来额外的编排复杂度、Agent 间通信开销、状态管理成本,还有调试难度。如果你的任务就是"查个天气、算个数、格式化一段文本",单体 Agent 甚至一个普通函数调用就够了,硬拆成多 Agent 纯属自找麻烦。

我给自己定的判断标准是:如果单体 Agent 在典型输入下,上下文占用稳定低于模型窗口的 30%,且任务步骤少于 5 步,就不拆。这个阈值不是绝对的,但能过滤掉大部分"为了 Multi-Agent 而 Multi-Agent"的情况。

5.2 拆分的收益来自"职责冲突",不是"步骤多"

有些任务步骤很多,但职责不冲突。比如"批量重命名 100 个文件",步骤多但每步都一样,单体 Agent 加个循环就搞定,拆成多 Agent 反而增加协调成本。真正需要拆的,是那种规划、执行、校验三种职责互相打架的任务。判断方法很简单:如果你发现单体 Agent 的 prompt 里出现了"既要……又要……还要……"这种句式,那就是拆分信号。

5.3 先优化单体,再考虑拆分

在拆之前,先把单体能优化的地方榨干:工具返回做聚合、历史做分层摘要、prompt 做职责聚焦、关键约束做显式复述。很多时候单体 Agent 表现差,不是架构问题,是上下文管理没做好。我见过不少项目,把工具返回值从"全量"改成"摘要 + 按需拉取"之后,单体 Agent 的成功率直接翻倍,根本不需要上 Multi-Agent。

注意:Multi-Agent 是解决"职责冲突 + 上下文污染"的手段,不是解决"模型能力不足"的万能药。模型本身不行,拆成十个 Agent 还是不行。

6. 落地一套最小可用的 Multi-Agent:Python 实操拆解

6.1 先定义契约,再写 Agent

我习惯从契约开始写,而不是从 Agent 开始。先定义好每个 Agent 的输入输出结构,用 Python 的dataclass或pydantic模型固定下来。比如:

from pydantic import BaseModel from typing import List, Optional class SubTask(BaseModel): task_id: str description: str depends_on: List[str] = [] acceptance: str class Plan(BaseModel): goal: str subtasks: List[SubTask] class ExecResult(BaseModel): task_id: str status: str # success / failed / blocked artifact: Optional[str] = None notes: str = "" class ReviewResult(BaseModel): task_id: str passed: bool issues: List[str] = []

契约定好之后,每个 Agent 的 prompt 里直接嵌入对应的 schema 说明,要求模型输出严格符合结构的 JSON。这样编排层拿到结果就能直接解析,不需要再做自然语言抽取。这一步做扎实,后面能省掉大量调试时间。

6.2 规划 Agent:把模糊目标拆成可验收的子任务

规划 Agent 的 prompt 核心是"拆得够细、依赖标清楚、验收标准可执行"。我通常要求它输出 3 到 8 个子任务,每个子任务的acceptance必须是一个可判断真假的陈述,比如"文件 X 中不再出现裸except:"而不是"代码质量提升"。

这里有个经验:规划 Agent 不要让它执行任何工具。它的上下文里只有目标和约束,保持干净。一旦让它调工具,上下文就会被污染,规划质量下降。规划就是规划,执行交给执行 Agent。

6.3 执行 Agent:一次只做一件事,做完就交棒

执行 Agent 的输入是单个子任务加必要的前置产出,输出是ExecResult。它的 prompt 要强调"只做当前子任务、遇到不确定就返回 blocked、不要自行扩大范围"。我踩过的坑是:执行 Agent 太"聪明",看到当前任务简单就顺手把后面的任务也做了,结果和后续执行冲突。后来在 prompt 里明确写"禁止处理当前子任务之外的任何内容",问题才解决。

执行 Agent 的上下文里,前置产出要按需注入,不要全量塞。比如当前子任务只依赖第 2 步的输出,就只注入第 2 步的 artifact,其他步骤的产出不进上下文。这是控制上下文的关键操作。

6.4 校验 Agent:默认产出有问题,逐条对照验收标准

校验 Agent 的 prompt 要"苛刻"。我通常这样写:"你是一个严格的质检员,默认产出物存在问题。请逐条对照验收标准,找出至少一个具体问题,或明确说明为什么产出物完全合格。"这种"默认怀疑"的设定,能显著提高问题检出率。

校验 Agent 的输入是产出物加验收标准,输出是ReviewResult。如果passed为 false,编排层根据issues决定是让执行 Agent 重试,还是升级给规划 Agent 重新拆解。校验 Agent 不负责修复,只负责判断,职责边界要清晰。

6.5 编排器:状态机加任务队列

编排器我一般用一个简单的状态机实现。核心逻辑是:按依赖顺序取任务,注入前置产出,调用执行 Agent,调用校验 Agent,根据结果决定推进或重试。重试超过阈值就标记失败并终止。

def run_pipeline(plan: Plan, max_retry: int = 2): state = {t.task_id: {"status": "pending", "output": None} for t in plan.subtasks} for task in plan.subtasks: if not all(state[d]["status"] == "success" for d in task.depends_on): state[task.task_id]["status"] = "blocked" continue for attempt in range(max_retry + 1): inputs = {d: state[d]["output"] for d in task.depends_on} result = execute_agent(task, inputs) if result.status != "success": continue review = review_agent(task, result.artifact) if review.passed: state[task.task_id] = {"status": "success", "output": result.artifact} break else: state[task.task_id]["status"] = "failed" break return state

这段代码很朴素,但把状态显式化、把重试逻辑集中化之后,整个系统的行为就变得可预测、可观测。出问题时打印state就能定位到具体环节。

7. 踩过的坑与实测经验:Multi-Agent 不是银弹

7.1 Agent 数量不是越多越好

我一开始也犯过"多拆几个 Agent 总没错"的错,把一个任务拆成 7 个 Agent,结果协调开销爆炸,光 Agent 间传递上下文就占了大量 token,整体耗时比单体还长。后来收敛到 3 到 4 个核心 Agent(规划、执行、校验,必要时加一个汇总),效果反而最好。每个 Agent 都应该有明确的、不可合并的职责,否则就是负担。

7.2 上下文注入要"最小必要",不是"越多越好"

执行 Agent 的上下文里,前置产出只注入当前子任务真正需要的部分。我见过有人把全局状态全量注入每个 Agent,结果每个 Agent 的上下文都很长,退化问题和单体一样。Multi-Agent 的上下文优势,来自"每个 Agent 只看自己需要的",一旦全量注入,优势就没了。

7.3 校验 Agent 也会"放水",需要对抗性设计

校验 Agent 如果 prompt 写得太温和,会倾向于说"看起来没问题"。我试过几种对抗性设计:一是要求它必须给出至少一个具体问题或明确说明合格理由;二是让它先列出验收标准的每一条,再逐条判断;三是引入第二个校验 Agent 做交叉检查。实测下来,第一种加第二种组合性价比最高,第三种只在关键任务上用。

7.4 失败重试要有上限,且要区分失败类型

不是所有失败都值得重试。执行 Agent 返回blocked(信息不足)和返回failed(执行出错)应该区别对待:blocked可能需要补充输入或升级给规划 Agent,failed可能重试一次就好。我一般设max_retry=2,超过就终止并输出当前状态,让人介入。无上限重试是 token 黑洞。

7.5 可观测性比性能更重要

Multi-Agent 系统里,最值钱的能力是"能看清发生了什么"。我习惯给每个 Agent 调用打日志:输入、输出、耗时、token 消耗、重试次数。这些日志在调试时比任何性能指标都有用。有一次一个任务反复失败,看日志才发现是校验 Agent 的验收标准里有个字段名写错了,导致永远判不过。没有可观测性,Multi-Agent 就是个黑盒。

8. 从单体到多体的迁移路径:别一步到位

如果你现在有一个跑得还行的单体 Agent,想迁移到 Multi-Agent,我的建议是渐进式迁移,不要推倒重来。

第一步,先把单体 Agent 里的"校验"环节抽出来,做成一个独立的校验 Agent。这一步改动最小,收益却很明显,因为校验独立后,主 Agent 的上下文会短一截,而且校验质量会提升。

第二步,把"规划"抽出来。让规划 Agent 先产出结构化计划,主 Agent 按计划执行。这一步会让任务的可控性大幅提升,因为计划是显式的、可检查的。

第三步,把"执行"按子任务拆开,每个子任务独立上下文。到这一步,你就有了一个完整的 Multi-Agent 流水线。

每一步都可以独立验证效果,出问题也能快速回滚。我自己的项目就是这么一步步迁过来的,中间没有出现过"大爆炸式重构"的风险。

最后分享一个我常用的判断技巧:当你发现单体 Agent 的 prompt 里开始出现"在……之后,再……,同时注意……"这种多重约束叠加的句子时,就是该拆的信号了。因为这说明单一上下文已经承载不了多重职责,模型在被迫做它不擅长的权衡。这时候拆开,让每个 Agent 只背一个约束,效果往往立竿见影。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进