1. 先想清楚:为什么我会把单体 Agent 拆成 SubAgent
1.1 从一次“翻车”说起
上个月我在折腾一个内部知识库问答助手,一开始图省事,所有逻辑都用单个 Agent 搞定:一个 system prompt 里塞进了“意图识别”“知识检索”“摘要生成”“格式整理”四套职责,工具(tools)也挂了一堆,包括向量检索、SQL 查询、邮件发送。表面上看功能齐全,实测一跑就露馅:用户问一句“上季度销售数据怎么样,帮我整理成邮件发给市场部”,它先调了向量检索,又去查了 SQL,最后生成的邮件里数据竟然是错的。更麻烦的是,我根本不知道它在哪一步犯的错——日志里只有一串串模型输出,没有清晰的任务边界。
后来我把这个单体 Agent 拆成了几个 SubAgent,用 Microsoft Agent Framework 的 Multi-Agent 编排能力重新搭了一遍:一个控制器 Agent 负责拆任务,三个 SubAgent 分别负责“查文档”“查数据”“写文案”。问题立刻清楚了很多,模型犯错的概率也肉眼可见地下降了。这就是这篇文章的由来:我想把 SubAgent 的决策思路、架构设计、代码实现和调试心得一次讲透,给准备上手 Multi-Agent 的同学一份能直接参考的实践笔记。
1.2 单体 Agent 的四个典型毛病
先说结论:不是所有场景都需要 SubAgent,但当你发现单体 Agent 开始“带不动”的时候,通常跑不掉下面四个症状。
第一是 system prompt 膨胀。任务一多,你不得不在提示词里写大量分支规则:“如果用户问 A,就调用 X;如果用户问 B,就调用 Y;如果用户问 C,先做 Z 再调用 W……”这些规则叠加起来,模型上下文里塞满了指令,真正有用的业务知识反而被挤到边缘,模型开始“记混”。我见过有人把 system prompt 写到 6000 多字,效果依然稀烂。
第二是上下文污染。单 Agent 处理多步骤任务时,中间结果会源源不断写进同一个对话历史里。比如先查文档,再把文档内容拿去生成表格,最后又要总结成邮件——前面的文档正文、表格中间态全在一个上下文里滚来滚去,模型很容易被无关信息带偏,甚至在最终答案里引用已经被否掉的中间数据。
第三是工具选错。工具一多,模型需要做的“路由决策”就越复杂。拿我那个例子来说,查向量库和查 SQL 在模型眼里看起来都像“检索”,但它俩语义完全不同。模型一旦选错工具,后面所有步骤都建立在错误基座上,且这种错误非常隐蔽,不仔细核对输出根本发现不了。
第四是难排查。单体 Agent 是一个黑盒:用户说了一个需求,它内部经历了什么,只有模型自己知道。调试时你只能看到最终输出,中间步骤的错误被层层包装,想定位“是哪一步导致结果不对”往往要反复试很多次,非常消耗耐心和 token。
1.3 SubAgent 到底解决了什么问题
SubAgent 的核心思路,是把一个大而全的 Agent 拆成一个小而专的 Agent 团队:一个负责调度的控制器(Controller)加若干个各司其职的子代理(SubAgent)。每个 SubAgent 只保留单一职责、独立的 system prompt、独立的上下文窗口,甚至可以用不同的模型。这样一来,上述四个问题被逐个击破。
上下文被隔离了。每个 SubAgent 只看到自己的输入和输出,不会把其他环节的中间产物都背在身上。还是那个例子:“查 SQL”的 SubAgent 只需要知道表结构和查询需求,它不需要看到“查文档”那个 SubAgent 贴进来的长篇文档内容。每个 Agent 的上下文都更干净,模型注意力也更集中。
路由决策变简单了。控制器虽然还是需要做意图判断,但它不需要在一大堆工具里做精细选择,只需要决定“把这个任务派给哪个 SubAgent”,或者按顺序依次派发。任务分配粒度变粗,模型决策压力骤降,正确率自然上去。
可观测性大大提升。多个 Agent 之间是通过消息传递协作的,每一条消息、每一次工具调用都能被记录和回放。哪个 SubAgent 在哪一步出了错,一眼就能看到。这种特性在调试复杂任务时简直是救命稻草。
扩展性也更好。新增一个能力,不需要去改那个已经 6000 字的大 prompt,只要新增一个 SubAgent,然后在控制器里加一条路由规则即可。团队里不同人可以并行维护不同的 SubAgent,互不干扰。
1.4 什么情况下不建议上 Multi-Agent
拆 Agent 有好处,但也有成本。如果你是新手、任务链路短、单 Agent 已经能稳定跑通,我劝你先别拆。Multi-Agent 会引入新的复杂度:Agent 之间的通信需要设计、编排需要调试、token 消耗会上升(多个 Agent 各算各的上下文),出了问题排查链路也变长了。我见过不少团队为了“技术先进”硬上 Multi-Agent,最后连一个简单的入口问答都做不稳定。
一个比较务实的判断标准是:如果你的 system prompt 字数低于 2000,工具少于 3 个,任务步骤不超过 3 步,那就继续用单体 Agent。等你真的感受到单 Agent 的瓶颈,再考虑拆分。拆的时候也不要一步到位,先把最容易出错的一个环节单独抽出来做成 SubAgent,跑稳定了再往下推。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 案例拆解:SubAgent 架构怎么设计才不翻车
2.1 案例目标:一周工作日志生成周报
为了把代码讲清楚,我拿一个非常典型的场景来设计:周报生成助手。输入是一周的工作日志,杂乱无章的流水账,比如“周一:和某客户沟通需求,确认排期;周二:写了用户模块的接口文档;周三:修复线上 bug,根因是缓存失效……”输出是一份结构清晰、重点突出、带数据佐证的周报。
这个任务看起来简单,但单 Agent 做很容易出问题:一是流水账里有效信息密度低,模型容易把无关琐事也写进周报;二是周报需要区分“事项”“进展”“数据”“风险”,模型需要做多步信息抽取;三是如果工作日志里提到了指标数字(比如“接口耗时从 500ms 降到 200ms”),模型要能识别并显式呈现。拆成 SubAgent 之后,每个环节的提示词都可以做得非常专注,效果明显提升。
为了体现 Multi-Agent 的编排价值,我给这个系统设计了四个角色:控制器、需求分析 SubAgent、数据整理 SubAgent、文案生成 SubAgent。控制器不是直接干活的,它是“项目经理”,负责任务拆解、人员调度、结果审核。
2.2 控制器 + 3 个 SubAgent 的角色分工
在设计 SubAgent 架构时,最重要的一件事就是划清每个 Agent 的职责边界。边界模糊是 Multi-Agent 系统最常见的病根。下面是我在这个案例里的分工表:
| Agent 名称 | 角色定位 | 输入 | 输出 | 关键约束 |
|---|---|---|---|---|
| Controller(控制器) | 项目经理 | 用户原始需求 | 最终周报 | 不直接撰写正文,只负责拆解、派发、汇总 |
| 需求分析 SubAgent | 信息抽取员 | 原始工作日志 | 结构化事件清单 | 只输出事项和关键信息,不做评价 |
| 数据整理 SubAgent | 数据专员 | 结构化事件清单 | 指标变更表 | 只处理数字指标,不写叙事 |
| 文案生成 SubAgent | 执笔人 | 事件清单 + 指标表 | 周报初稿 | 只负责文字组织,不新增信息 |
我特别强调“Controller 不直接写正文”这个约束。因为控制器一旦开始写正文,它就成了又一个单体 Agent,SubAgent 的专精优势就没了。控制器的提示词里我会写得很死:“你只负责调度和格式整合,不要自行生成业务内容。你需要把任务拆成子任务,分配给对应的 SubAgent,收集返回结果,再拼接成最终输出。”实测下来,把这条规则写清楚,能避免大量“控制器大包大揽”导致的混乱。
2.3 消息流转与终止条件设计
SubAgent 之间的协作方式,我建议先想清楚“消息怎么流”,再写代码。这个案例的流转分两条线。
第一条线是串行主线:Controller 收到用户原始日志后,先把它派发给需求分析 SubAgent;需求分析 SubAgent 返回结构化事件清单;Controller 把清单同时派给数据整理 SubAgent 和文案生成 SubAgent(这两步可以并行,也可以串行,看模型和成本约束);数据整理返回指标变更表,文案生成返回周报初稿;Controller 最后把事件清单里的“重点事项”、指标表里的“数据变化”和文案初稿整合成最终周报。
第二条线是异常回退:如果数据整理 SubAgent 发现输入里没有足够的数字指标,它应该明确返回“本次日志中未发现可量化的指标数据”,而不是强行编一个数字。Controller 收到这种反馈后,会在最终周报里如实写“本周暂无明显量化指标”,而不是让文案生成 SubAgent 硬凑。
还有一个容易忽略的设计点:终止条件。Agent 之间来回对话,如果没人喊停,可能陷入无限循环。我通常会用两种终止条件的组合:一种是消息条数上限(比如最多 10 条消息强制结束),另一种是“标记词”终止(某个 Agent 输出“完成”二字就结束)。这两个条件加起来,能兜住绝大多数失控场景。
3. 代码实战:一个可运行的 SubAgent 系统
3.1 环境准备与模型客户端配置
下面的代码我基于 Microsoft Agent Framework 底层的 Python 运行时(AutoGen 底座)写,版本是 0.4.x 这一代。别担心,Agent 的抽象思路和具体 API 版本关系不大,就算你拿到的新版本方法名略有变化,核心的模式“定义 Agent → 组成团队 → 设置终止条件 → 跑起来”是不变的。
先建虚拟环境、装依赖:
bash复制python -m venv .venv
source .venv/bin/activate # Windows 下用 .venv\Scripts\activate
pip install autogen-agentchat autogen-ext[openai]
模型客户端我建议先统一用同一个模型,比如 gpt-4o-mini 或 qwen-plus。等系统跑通了,再针对不同 SubAgent 换不同模型做成本和效果优化。配置方式如下:
python复制import asyncio
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.teams import SelectorGroupChat
from autogen_agentchat.conditions import MaxMessageTermination, TextMentionTermination
from autogen_ext.models.openai import OpenAIChatCompletionClient
model_client = OpenAIChatCompletionClient(
model="gpt-4o-mini",
api_key="YOUR_API_KEY",
)
如果你用 Azure OpenAI,把客户端换成 AzureOpenAIChatCompletionClient,填入 azure_endpoint、api_version、model 等参数即可。注意一点:所有 SubAgent 共用同一个 model_client 对象没问题,但如果某个 SubAgent 对格式准确性要求特别高,我建议单独给它配一个更强的模型(比如 gpt-4o),单独建一个 client 实例。
3.2 实现三个 SubAgent
SubAgent 的定义其实很简单,关键在于 system prompt 怎么写。我总结的写法是:身份 + 输入约定 + 输出格式 + 禁忌。
需求分析 SubAgent 的代码:
python复制analyst_agent = AssistantAgent(
name="Analyst",
model_client=model_client,
system_message=(
"你是一名需求分析专家。你的任务是阅读用户提供的工作日志,"
"提取出其中重要的业务事件、项目进展和关键风险。"
"输入:一段或多段原始工作日志。"
"输出:按以下格式输出结构化事件清单:\n"
"1. [重要事件] 事件描述\n"
"2. [项目进展] 进展描述\n"
"3. [风险问题] 问题描述\n"
"如果某类信息不存在,请明确写'无'。"
"注意:你只做提取,不做评价,不要给出改进建议,也不要重写日志内容。"
),
)
数据整理 SubAgent 的代码:
python复制data_agent = AssistantAgent(
name="DataAnalyst",
model_client=model_client,
system_message=(
"你是一名数据整理专员。你的任务是阅读结构化事件清单,"
"找出所有包含数字指标的描述,例如'耗时降低20%'、'接口QPS从100升到300'等。"
"输出格式:\n"
"指标名 | 变化方向 | 量化数值 | 原始描述\n"
"如果没有找到任何量化指标,请只输出'未发现量化指标',不要编造数据。"
"你不负责写周报正文,只需要输出指标表格。"
),
)
文案生成 SubAgent 的代码:
python复制writer_agent = AssistantAgent(
name="Writer",
model_client=model_client,
system_message=(
"你是一名周报撰写专家。你手里会拿到结构化事件清单和指标表格,"
"请你生成一份结构清晰的周报正文。周报包括:本周重点事项、项目进展、数据表现、风险问题。"
"要求:语言简洁,每个事项用一两句话描述;数据必须引用输入表格中的内容,"
"不得自行添加未提供的数据;不要编造任何内容。"
"输出纯文本周报,不要输出JSON或markdown代码块。"
),
)
写这三个 agent 的时候,我踩过一个很典型的坑:早期版本我让“需求分析 SubAgent”直接输出周报正文,结果它和“文案生成 SubAgent”的输出高度重叠,Controller 都不知道该听谁的。后来我把边界改成了“分析师只输出结构化清单,写手才输出正文”,问题立刻消失。所以 SubAgent 的 system prompt 里最好都写明“你不是谁,你不做什么”,这个负面约束比正面要求还重要。
3.3 实现 Controller 与团队编排
Controller 我用 AssistantAgent 来实现,但它的职责是“派活”而不是“干活”。它的 system prompt 是整套系统的灵魂:
python复制controller = AssistantAgent(
name="Controller",
model_client=model_client,
system_message=(
"你是一个多Agent系统的控制器,负责把用户需求拆解给其他Agent执行。"
"你有三个下属Agent:Analyst负责把原始日志整理成结构化事件清单;"
"DataAnalyst负责提取量化指标;Writer负责撰写周报正文。"
"执行流程:第一步,把用户提供的日志发送给Analyst;"
"第二步,把Analyst的输出同时发送给DataAnalyst和Writer;"
"第三步,把Analyst、DataAnalyst、Writer的输出整合为最终周报。"
"整合周报时,你只能调整排版和结构,不能修改数据。"
"当最终周报输出完毕后,请输出'完成'两个字。"
),
)
接下来把四个 Agent 组成一个群聊团队。我比较推荐 SelectorGroupChat,它允许一个“选择器”决定下一轮由谁发言,比单纯的轮流发言(RoundRobinGroupChat)更适合有明确流程的编排任务。
python复制termination = (
MaxMessageTermination(max_messages=12)
| TextMentionTermination("完成")
)
team = SelectorGroupChat(
agents=[controller, analyst_agent, data_agent, writer_agent],
model_client=model_client,
termination_condition=termination,
)
这里有两个细节。第一个是终止条件,max_messages=12 是兜底,防止 Agent 之间无限对话;TextMentionTermination("完成") 是正常结束条件,只要 Controller 输出了“完成”,团队就停止。第二个是 SelectorGroupChat 的选择器默认也是由模型担任,它每一次对话前都会判断“当前状态轮到谁发言”。为了让选择器判断准,各 Agent 的名称一定要语义化,别用 agent1、agent2,否则模型很容易选错人。
3.4 跑起来:执行与结果聚合
主流程用异步方式执行,实时打印每一条消息,方便观察谁在什么时候说了什么:
python复制async def main():
task = (
"用户一周工作日志:\n"
"周一:与客户确认需求排期,确定下月上线时间。\n"
"周二:完成用户模块接口文档编写。\n"
"周三:修复线上缓存失效bug,接口耗时从500ms降到200ms。\n"
"周四:配合测试同学进行回归测试。\n"
"周五:整理项目风险清单,发现第三方支付接口可能存在稳定性问题。"
)
async for message in team.run_stream(task=task):
if message.source:
print(f"[{message.source}] -> {message.content}")
asyncio.run(main())
跑完之后,团队会先由 Controller 派单,Analyst 返回结构化清单,DataAnalyst 返回指标表,Writer 返回周报正文,最后 Controller 汇总并输出“完成”。你会在终端看到清晰的 Agent 协作过程。这个过程本身就是调试利器——如果某个环节输出不对,你能直接定位到是哪个 Agent 的问题。
如果你想拿到最终结果而不是只打印,可以收集最后一条来自 Controller 的完整消息,落盘保存:
python复制async def main():
result = await team.run(task=task)
final_message = result.messages[-1]
print(final_message.content)
注意 team.run() 和 team.run_stream() 的区别:前者一次性返回全部消息,适合需要最终结果的场景;后者流式返回,适合观察过程。调试阶段我建议用 run_stream,上线以后用 run 减少 IO 开销。
4. 调试与优化:这些坑我都替你踩过
4.1 先学会看日志和中间输出
Multi-Agent 系统调试的第一件事,不是看最终结果,而是看中间每一步消息。我建议在开发环境把 run_stream 的每条消息都打印出来,并且强制打印 message.source。因为同一个模型可能在多个 Agent 中复用,光看内容看不出是谁说的,必须看来源。
如果你用的模型客户端支持 token 统计,务必顺手统计每个 Agent 消耗的 token 数。我的经验是:很多“效果不好”的问题,本质是某个 SubAgent 的上下文被撑爆,或者 Controller 反复派发同一任务导致 token 翻倍。把这些量级记下来,优化才有依据。
4.2 高频问题排查表
这里整理我在实践中遇到最多的几个问题,以及对应的排查思路:
| 现象 | 常见原因 | 排查方法 |
|---|---|---|
| Agent 之间来回对话停不下来 | 终止条件缺失或设置过宽 | 检查 termination_condition,把 max_messages 调小,确认结束标记词能否被模型正常输出 |
| Controller 自己把活干了,SubAgent 没参与 | Controller 的 system prompt 没有强调“只调度,不执行” | 重写 Controller 提示词,加入“不要自己生成业务内容”的负面约束 |
| 某个 SubAgent 输出格式不稳定 | system prompt 里格式说明不够具体 | 提供更详细的输出样例,甚至用 few-shot 格式示例 |
| 多个 SubAgent 输出之间有信息冲突 | 上游 Agent 传给下游的中间信息被污染 | 检查每一条消息的内容,确认下游 Agent 只收到它需要的上游输出 |
| 模型工具调用一直失败 | 工具参数没有按 JSON Schema 严格定义 | 检查工具函数的参数说明、必填字段、类型约束;尽量用简单扁平的结构 |
| 选择器模型选错发言人 | Agent 名称语义不明,或选择器模型能力偏弱 | 给 Agent 起语义明确的名称,比如 Analyst、DataAnalyst、Writer;必要时给选择器单独换更强的模型 |
| token 消耗超高 | 缺少终止条件、上下文反复传播、模型输出过长 | 缩短每个 Agent 的 system prompt,限制 max_tokens,必要时用便宜的模型处理低价值 SubAgent |
这些坑里,最容易被忽略的是第二条。Controller 大包大揽是所有 Multi-Agent 系统的通病,因为模型天然有“回答用户问题”的冲动。你要反复去敲打它的 role 定位,甚至在 system prompt 里加一句“如果你发现自己在撰写正文或计算数据,请停下来,把这些工作交给对应的 SubAgent”。
4.3 成本与模型选型优化
Multi-Agent 的成本确实比单 Agent 高,但高多少完全取决于你的设计。我给出几个低成本原则。
能用小模型就不用大模型。SubAgent 的任务边界清楚、上下文干净,对模型智商的要求往往比单体 Agent 低。我的习惯是:Controller 和需要做复杂推理的 SubAgent 用强模型;纯格式转换、信息提取的 SubAgent 用便宜的小模型。你完全可以在定义每个 Agent 时传入不同的 model_client。
严格控制每个 SubAgent 的 max_tokens。信息抽取类任务给 300-500 token 就够,没必要让模型写小作文。把这个参数写进 Agent 定义里,能省下大量无效输出。
消息长度会显著影响下一轮的 token 消耗。因为每次轮到 Controller 发言时,它需要把之前所有消息“重新读一遍”。所以不要让 SubAgent 输出长篇大论,中间输出能短则短。我前面让 Analyst 输出“结构化清单”而不是“完整分析报告”,一部分原因就在这里。
最后,还有一个反直觉的经验:在调试阶段不要过度优化 token。先把流程跑通、把效果调对,再回头压缩提示词、换小模型。否则你会在“输出被截断”和“效果变差”之间来回折腾,浪费时间。
4.4 从单 Agent 平滑迁移到 SubAgent 的路径
如果你现在手里已经有一个能跑的单体 Agent,不要推倒重来,我建议按这个路径渐进迁移。
先把单体 Agent 的输出切块。观察它的 system prompt,看哪些步骤可以被切出去。通常最容易切的是最后一步“格式整理”或“文案润色”,因为它对上下文依赖最小,切出去后风险也最低。
再做成一个“Controller + 一个 SubAgent”的最小结构。Controller 保留原有的大部分逻辑,把切出去的那一步变成调用 SubAgent。跑通后,再继续切第二个。每次只动一步,出问题容易回滚,也容易定位。
最后再调整路由方案。当你有了两个以上 SubAgent,才开始正式设计 Controller 的路由规则和终止条件。这个时候再选 SelectorGroupChat 或更复杂的图编排也不迟。
我自己就是从“一个 Agent 干了 80% 的活”慢慢演化到“Controller 只干调度和汇总”的。每切一步,我都会拿同一批测试用例回归一遍,确保输出质量没有下降。这套渐进式迁移方式,比一次到位稳妥得多,建议你也试试。
我个人在实际操作中的一个很深的体会是:SubAgent 架构不是为了让系统显得“高级”,而是为了让每个环节更简单、更可控、更好调试。如果你拆完 Agent 之后,发现每个 Agent 的 prompt 依然又长又乱,那说明拆分粒度不对,应该继续切细或者重新划边界。等到你看着每个 SubAgent 的提示词都能一眼读懂它负责什么、不负责什么,这个架构才算真正立住了。
