1. 为什么需要 SubAgent:从单代理到多代理的转变
先聊一个很实际的感受。我最初用 Microsoft Agent Framework(MAF)搭应用时,习惯把所有逻辑塞进一个 Agent 里:写代码让主 Agent 直接生成代码,让它自己检查,自己修正。结果并不理想,不是模型能力不够,而是职责混在一起之后,提示词非常长,上下文很容易被中间步骤污染。一会儿模型忘了最初的格式要求,一会儿又把子问题无限放大,明明一个几百行的小功能,来回折腾好几轮,输出还不稳定。
后来我开始尝试把“子任务”拆给 SubAgent,也就是多代理架构里的“主从模式”,情况立刻不一样了。主 Agent 只负责理解用户需求、拆分任务、汇总结果;真正干活的 Coder、Reviewer、Searcher 都是独立 Agent,按需被主 Agent 唤起。这套思路用在 Microsoft Agent Framework 上尤其顺手,因为它把 Agent、Task、Thread 这些概念都抽象得比较干净,适合做多角色协作。
那 SubAgent 到底解决了什么问题?简单说,它解决了三类事:第一是职责隔离,每个子代理只维护自己领域的上下文和指令,不会互相干扰;第二是提示词工程从“一口大锅”变成“小灶单烧”,复杂需求可以被拆成多个小而专业的任务;第三是可扩展性,你想加一个新的专业角色,只写一个新 Agent 就行,改造主 Agent 的调度逻辑几乎不用动。
这篇文章适合谁?已经跑通过 MAF 单 Agent,想让架构更可控的人;正在做代码生成、文档处理、数据分析这类复杂任务的开发者;还有想理解 Multi-Agent 编排设计,但不想一上来就看源码的人。我会从 MAF 的基础概念开始讲,再落到一个可运行的 SubAgent 协作案例上,最后把容易踩的坑都列出来。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 认识 Microsoft Agent Framework 中的基础编排概念
2.1 Agent 不再只是“聊天封装”这么简单
MAF 底层把 Agent 抽象成了可执行单元,简单理解就是:Agent 是一个接收任务、内部调用模型和工具、最终产出结果的对象。这个抽象和以前我写“把 API key 和 system prompt 塞进函数里”完全不同,官方把 Agent 本身的运行生命周期、消息传递、终止条件都管了起来。
在实际代码里,常用的是 AgentBase 这个基类,你只需要在里面定义自己的处理逻辑,比如重写运行方法或者用声明式的方式配置模型。我最欣赏的设计是它引入了 Task 和 AgentThread,这个组合对应到生活里就是“需求和会话”:Task 描述你要 Agent 完成什么目标,AgentThread 负责实际跑一轮对话循环,管理模型调用、工具调用、终止判断这些繁琐过程。
python复制from microsoft.agent_framework.core import AgentBase
class ReportAgent(AgentBase):
def __init__(self, name: str):
super().__init__(name=name, system_message="你负责生成结构化报告")
async def run(self, task_description: str) -> str:
# 这里的执行细节可以自行封装
return await self._invoke_model(task_description)
这样说可能比较抽象,换个角度来看:传统代码里,如果你想让模型“先思考再调用工具再总结”,每个环节都要自己写状态机;用 MAF 之后,你告诉 AgentThread 用哪个 Agent 去完成哪个 Task,剩下的模型上下文组装、工具结果回填、是否还需要继续执行,由框架来承担。你可以把它当成一个更聪明的“for 循环”,而 Agent 就是循环体。
2.2 两种工作方式:声明式配置与代码优先
MAF 经常让人困惑的一点是它提供了两种使用路径。一种偏声明式,你可以在配置文件里定义 Agent 的模型、参数、工具,然后通过框架加载;另一种是纯代码方式,你直接实例化 Agent,用代码把编排逻辑写清楚。两种没有绝对优劣,主要看场景。
声明式的好处是变更快、复制简单,比较适合“Agent 角色相对固定”的团队协作场景。你定义一个 Coder Agent、一个 Reviewer Agent,把它们的提示词写在 yaml 或 json 里,其他人不需要读懂代码也能改角色设定。代码优先则适合需要动态生成 Agent 的场景,比如根据外部输入决定要不要增加一个额外的质检 Agent。
我个人的建议是:核心业务不建议全走声明式。因为 SubAgent 之间往往需要共享一些变量或做条件判断,纯声明式表达起来很别扭。比较好的折中是主调度用代码写清楚,角色配置抽成常量或配置文件,两头都不耽误。
2.3 Multi-Agent 与 SubAgent 的层级关系
做 Multi-Agent 时,系统里的结构不一定是一堆扁平化的 Agent 互相聊天。无约束的对等 Agent 群在真实项目里非常难控制,因为每个 Agent 都能看到别人的消息,为了一个目标反复争辩的话,token 消耗和响应延迟都会指数上升。MAF 比较推荐的模式是层级化:主 Agent 负责协调,SubAgent 像“后端团队”一样接收指令并返回结果。
SubAgent 的形态不外乎三类:
- 专业执行者:例如 Coder,只管生成和修改代码文件。
- 专家评审者:例如 Reviewer,只负责挑错、给建议,不改代码。
- 信息收集者:例如 RAG 搜索 Agent,只管从指定数据源找出相关内容。
从命名上说,“SubAgent”并不代表它能力差,而是说它位于主 Agent 的调度范围之内。真正决定项目上限的,是你怎么定义这些 Agent 之间的边界和交接规则。上下文切换是成本最高的地方,所以要尽量把边界切在“信息类型明显不同”的地方。
3. SubAgent 的核心设计思想:主从模式与“Agent 即工具”
3.1 为什么主从模式是当前多 Agent 方案里的主流选择
如果你去翻最近几个流行框架的设计文档,会发现主从(Supervisor/Worker)模式几乎是大家不约而同的选择。表面上看起来“让多个 Agent 自由辩论”更高级,但落地时问题非常大:每个 Agent 都有自己的人格设定,讨论来讨论去很容易陷入互相否定,而且很难判断什么时候该停止。
主从模式把决策权集中到一个主 Agent 上,它不是一个“万事通”,而是一个“项目经理”。它未必知道具体代码怎么写,但它知道该找谁写;拿到结果后,它也有能力判断要不要交给评审 Agent 再过一道。这样整个系统的控制流是收敛的,不会出现发散到不可控的问题。
这一点在 MAF 里尤其好实现,因为 Agent 之间不是靠广播消息来交互的,而是通过一次明确的 Task 下发和结果回收来完成。换句话说,SubAgent 更像是“主 Agent 手里拿着的一份能力清单”,主 Agent 依据任务内容决定调用哪一个能力。这种“控制集中在中心,专业能力分布在外围”的结构,天生就是主从模式。
3.2 SubAgent 本质上是特殊 Tool
最近我在设计多 Agent 的时候,越来越认可一个说法:与其把 SubAgent 当成一个对等的对话者,不如把它当成一个“有智能的工具”来调用。为什么这么说?你看传统 Tool 的本质:输入一段参数,经过一段确定性的逻辑,返回结构化结果。而 SubAgent 的差异只是中间那段逻辑不是写死的代码,而是“由模型根据上下文动态推理”的过程。
当你把 SubAgent 看作 Tool 时,很多设计决策都会变得清晰。比如你会给 Tool 写清晰的功能描述和参数 schema,那你也应该给 SubAgent 写一套明确的“调用说明”;你会担心 Tool 的超时,那你也要给 SubAgent 设定时间预算;你会考虑 Tool 返回结果太长会占上下文,那你自然也会想办法让 SubAgent 返回摘要而不是几千字的大白话。
代码里的体现就是:主 Agent 的工具列表里可以挂一个 sub_agent_tool,它的执行体不是简单调用某个函数,而是把参数交给一个独立的 AgentThread 去跑。主 Agent 并不感知这里面还有另一个模型在循环,它只知道“这个工具返回了一段文本”。这就把多 Agent 的动态不确定性,重新纳入到了单 Agent 的工具调度框架里,工程上会稳很多。
我可以给出一个非常简化的示意:
python复制async def run_subagent_tool(task_description: str) -> str:
sub_task = Task(agent=coder_agent, description=task_description)
result = await AgentThread(task=sub_task).start()
return result.result_text
3.3 “视作 Tool”带来的设计取舍
把 SubAgent 视作 Tool,并不只是为了概念统一,它实际影响了你对失败的处理方案。Tool 调用失败,常见策略是换参数重试或直接报错;那 SubAgent 失败呢?你同样不应该让主 Agent 无限地和它周旋,而是应该让主 Agent 获得一段失败信息,由主 Agent 判断是换一种表述重新调用,还是跳过这个子任务。
另一个取舍在于并发策略。普通 Tool 是快进快出,SubAgent 通常会更慢,因为它在内部还要做多轮模型推理。所以你在编排时得考虑:我是一次只跑一个 SubAgent,还是把几个互相不依赖的 SubAgent 并发执行?从工程上讲,并行能显著降低整体响应时间,但是对资源占用和上下文策略都有要求。
从 API 设计的角度,我建议你给每个 SubAgent 定义一个类似“能力卡片”的说明:它擅长什么、不擅长什么、输入是什么格式、输出大概是什么样子、期望耗时多少。主 Agent 在真正调用之前会先看这些描述来规划,这就把多 Agent 系统的内部“路由”问题简化成了工具选择问题。很多早期 Multi-Agent 项目失控,正是因为缺少这层“工具化”的抽象,导致角色之间的调用混乱。
4. 动手实践:用 MAF 构建一个 Coder 与 Reviewer 协作系统
4.1 场景定界:主 Agent 怎么完成任务调度
讲完理论,我拿实际项目来演示。这个项目的目标很简单:用户抛出一个功能需求,主 Agent 调度一个 Coder SubAgent 写代码,再调度一个 Reviewer SubAgent 审查代码,如果审查有问题,主 Agent 把问题反馈给 Coder 要求修订,直到通过或达到最大轮数。
为什么不直接让主 Agent 自己写代码?因为“写代码”和“审代码”的心态完全不一样,写代码要大胆假设、快速产出,审代码要字字斟酌、带着批判性。这两种特性放在同一个 System Prompt 里会互相打架,模型很难在两个状态间自如切换。拆成两个 Agent 后,每个角色都能把自己的 System Prompt 用到极致。
场景里会涉及三个 Agent:
orchestrator:主 Agent,负责理解需求、派单、接收结果、决定是否完成。coder:SubAgent,负责生成或修改代码。reviewer:SubAgent,负责读代码、找问题、给修改意见。
Coder 和 Reviewer 不直接对话,所有消息都通过 orchestrator 中转,这样不会出现 Coder 说一句 Reviewer 顶一句的失控情况。
4.2 定义两个职责清晰的 SubAgent
用 MAF 定义 SubAgent 时,我不建议把所有提示词堆在一个超长字符串里,最好把角色的“身份、输入、输出、约束”拆开写。下面是一个 Coder Agent 的示例:
python复制from microsoft.agent_framework.core import AgentBase
CODER_SYSTEM_MESSAGE = """
你是一名严谨的 Python 工程师,负责根据需求编写高质量代码。
输入:一段功能需求描述,可能包含相关技术栈要求。
输出:可以直接运行的完整代码片段,以及必要的使用说明。
约束:
1. 优先考虑代码可读性,重要逻辑要加注释。
2. 不要臆造需求中不存在的 API,若不确定,请在输出开头注明假设。
3. 不负责排查业务逻辑以外的部署问题。
"""
async def run_coder_agent(requirement: str) -> str:
coder_agent = AgentBase(
name="coder",
system_message=CODER_SYSTEM_MESSAGE
)
task = Task(agent=coder_agent, description=requirement)
result = await AgentThread(task=task).start()
return result.result_text
Reviewer Agent 在思路上完全不同,它的 System Prompt 要强调负面思考。很多人写 Reviewer 的时候会忍不住让它“温和地给出建议”,真实效果反而不好,因为我希望它犀利一些,最好直接指出具体行号的问题。
python复制REVIEWER_SYSTEM_MESSAGE = """
你是一名代码审查专家,你的唯一目标是找出代码中的缺陷。
输入:一段代码及原始需求。
输出:发现的问题列表,每条包含问题描述、影响程度、修改建议。
如果代码没有问题,请输出:NO_ISSUES。
约束:
1. 不要提供新的实现,只指出问题和改进方向。
2. 重点关注边界条件、异常处理、安全风险、可维护性。
3. 如果没有把握的问题,标注为“建议确认”,不要占用主要问题数量。
"""
这样定义之后,你不用在主 Agent 的提示词里教它怎么写代码,也不用教它怎么Review,主 Agent 只需要知道“我有这两个子代理可以用”。
4.3 把 SubAgent 包装成可执行工具
为了让主 Agent 能在工具调用的框架里指挥 SubAgent,我给每个 SubAgent 都做了一层很薄的工具包装。这里的关键是工具描述要写得足够直白,因为主 Agent 是“读描述来决策”的,描述模糊它就不知道该在什么时候调用。
python复制async def coder_tool(requirement: str) -> str:
"""生成或修改代码。当用户需要新功能或现有代码需要重写时调用。"""
return await run_coder_agent(requirement)
async def reviewer_tool(code_snippet: str) -> str:
"""审查代码质量。当代码生成完成或修改完成后调用,返回问题列表。"""
return await run_reviewer_agent(code_snippet)
你注意看,这里我刻意没有把 SubAgent 的“内部提示词”暴露给主 Agent,主 Agent 能感知的是工具名、输入参数、返回格式。这正是“Agent 即 Tool”思路的体现:主 Agent 不需要知道工具内部是规则代码还是另一个大模型,它在决策层面把它们等同看待。
实际使用时,你再把这俩工具挂到主 Agent 的工具列表里。至于怎么挂,取决于你是用声明式配置还是代码注册,MAF 两种都支持。我比较喜欢代码注册的方式,因为后续可以动态增删工具,比如根据用户身份决定是否暴露某个 SubAgent,这在代码里就是一行 if 的事。
4.4 设计主调度循环:最多三轮修订
现在到了整个 Multi-Agent 案例最关键的部分:主 Agent 怎么决定流程结束。如果不设停止条件,很可能会出现 Coder 改完、Reviewer 又提新问题、Coder 再改、Reviewer 再提……这种死循环不光费 token,用户也很难等。我建议直接设定最大修订轮次,到了就直接返回当前最新的代码和未解决问题清单,让用户自己决定后续。
伪代码大概长这样:
python复制async def main_flow(user_requirement: str):
latest_code = await coder_tool(user_requirement)
for round_index in range(3):
review_feedback = await reviewer_tool(latest_code)
if "NO_ISSUES" in review_feedback:
return {"code": latest_code, "status": "passed", "rounds": round_index + 1}
latest_code = await coder_tool(
f"原始需求:{user_requirement}\n当前代码:{latest_code}\n"
f"请根据以下审查意见修改代码:\n{review_feedback}"
)
final_note = await reviewer_tool(latest_code)
return {"code": latest_code, "status": "partial", "review": final_note}
在真实的前后端架构里,这个 main_flow 不一定要让 orchestrator Agent 以“对话”形式执行。甚至整个循环都可以由传统的代码逻辑来控制,框架负责每一轮内部的任务执行。这就是我反复强调的好处:当 SubAgent 被包装成工具后,多 Agent 编排的“胶水代码”也能用传统的过程式逻辑写,排查问题的时候不用去猜模型在这一步到底想干嘛。
4.5 实测运行与效果观察
我拿一个真实需求跑了一遍:“写一个读取 CSV 文件、过滤掉空行、按指定列排序并输出为 JSON 的 Python 函数”。
第一轮 Coder 给出的实现基本能跑,但只处理了简单的空行,没考虑 CSV 中包含空字符串的单元格。Reviewer 很快就发现了这个问题,并指出排序时没有做类型转换,数字会被当成字符串排序。这让反馈质量比单纯让主 Agent“你自己再检查一下”要高得多,因为 Reviewer 的专注点只有找问题。
第二轮 Coder 在收到问题列表后,修改得很精准:加了 keep_default_na=False 参数,排序键里做了数值转换。Reviewer 复核后没有再揪出新问题,流程结束。整个过程大约耗时 40 秒,比单 Agent 自己边写边查大概多用了 15 秒,但结果是“一次成型”,省掉了我和它来回对话的时间。
这个测试也让我意识到一个事:两个独立 Agent 之间,消息传递的“接口设计”非常重要。第一版我把 Reviewer 的返回直接塞给 Coder 时,Coder 容易把“Reviewer 的话”当成“用户的新需求”,导致改偏方向。后来我在 Coder 的输入模板里明确加了“这是审查意见,不是新增需求”,跑起来就正常很多。这个小坑,不做多 Agent 项目根本发现不了。
5. 常见问题与排查实录
5.1 SubAgent 返回内容过长,把主 Agent 的上下文撑爆
这是我在多 Agent 项目里遇到概率最高的问题。Coder 生成几段代码还好,如果是 RAG 搜索 Agent,它能给你返回十来个文档片段,加起来几千 token。这些内容全部传回主 Agent,主 Agent 再做一次推理,上下文窗口很快就紧张了。
解决思路有两种。一种是让 SubAgent 在返回之前自己做一轮“压缩”,只返回和主 Agent 决策相关的摘要;另一种是 SubAgent 直接输出结构化摘要,而不是原始全文。我在实践里更倾向后者,让 SubAgent 把结果切成 summary、key_points、evidence 三段,主 Agent 大多数时候只要看 key_points 就够了。
python复制async def search_tool(query: str) -> dict:
raw_result = await run_search_agent(query)
return {
"summary": raw_result.summary,
"key_points": raw_result.key_points[:5],
"evidence": raw_result.source_passages[:2]
}
5.2 Coder 修完问题后新引入别的 Bug
多轮修订循环看起来合理,实际上存在一个隐患:Coder 为了解决 Reviewer 提出的第 1 个问题,可能把原本正确的第 2 个模块改坏了。Reviewer 第二轮如果只聚焦上一轮的问题,可能会漏掉新问题。
我目前的解法分两层。第一,Reviewer 在每轮都要完整再审一遍代码,而不是只看 diff,虽然成本高些,但能避免回归;第二,限定 Coder 的修改范围,在第二轮输入里明确告诉它“只需修改审查意见里提到的部分,不要重构无关代码”。这个提示能显著减少模型“顺手优化”的冲动。
5.3 子代理的并发执行与共享状态问题
如果一个主任务需要同时调用多个互不依赖的 SubAgent,你可以用 asyncio.gather 做并发。但并发会带来一个问题:如果这些 SubAgent 都会写一个共享缓存,就可能出现写冲突。MAF 本身不引入服务端组件的概念,本质上是你代码里的对象,所以并发安全性得靠自己保证。
我的建议是提前区分“只读子代理”和“写子代理”。检索类、审查类的可以并发;生成代码、写文件这种就别并发跑了,或者在任务里加上一个简单的全局锁。你不希望两个 Coder 同时在改同一个文件的不同部分,最后文件直接损坏。
另外,日志里建议给每个 SubAgent 加一个唯一的 trace_id,把主 Task 和若干子 Task 关联起来。没做这一步之前,多 Agent 项目一出问题,我光是搜日志都要半小时。
5.4 成本与延迟:比单 Agent 高是正常的,但要可控
拆成多个 SubAgent 之后,token 消耗肯定会比单 Agent 高,因为每一轮子任务都是独立调用模型,分子上下文自然就多了。你可以通过三个手段控制成本:限制最大轮数、让 SubAgent 输出精简格式、主 Agent 不要把整段原始代码回传给 Coder。
有一种让我觉得特别有效的做法是:主 Agent 收到 Coder 的结果后,不要直接转发给用户的全量需求,而是转为“变更概要”再传给 Reviewer。Reviewer 不需要知道用户最终想要什么,它只需要判断这一段代码是否符合工程标准。这样既保证了审查的独立性,也省了一大笔输入成本。
下面这张表是我整理的一个快速排查清单:
| 症状 | 可能原因 | 优先尝试的解决方向 |
|---|---|---|
| SubAgent 输出与需求偏离 | Tool 描述和任务描述不充分 | 在工具描述里写明输入输出格式与约束 |
| 多轮修订后代码变差 | 没有约束修改范围 | 提示 Coder 只修改审查意见中的内容 |
| 主 Agent 上下文溢出 | SubAgent 返回原始过载信息 | 让 SubAgent 返回摘要与关键点 |
| 任务卡住不结束 | 缺少终止条件或 Review 永远能发现问题 | 设定最大修订轮数,到点强制收敛 |
| 子任务之间互相干扰 | 用了共享缓存但没做隔离 | 只读型子任务并发,写型子任务串行 |
| 排查困难 | 没有统一 trace ID | 每个子任务附上主任务的 trace_id |
5.5 一个容易忽略的小技巧:给主 Agent 一个明确的“完成宣言”
多 Agent 系统最怕的不是 Agent 做错事,而是做完了却不汇报,或者一直觉得自己没做完。我习惯在最后一个环节让主 Agent 输出一个固定结构的“完成宣言”,里面包含最终结果、经历的修订轮次、仍然存在的风险和建议。这有点像软件开发里的 Definition of Done,一旦系统输出这个结构,就代表流程真正跑完了。
设计“完成宣言”还解决了一个用户体验问题:用户不需要从一堆子过程日志里翻找结论。主 Agent 直接告诉他“代码已生成,经过 1 次修订,已知风险有 1 条”,比把 Coder 和 Reviewer 的聊天记录全部倒给用户清爽得多。
text复制{"status": "passed", "rounds": 2, "risks": [], "summary": "..."}
6. 一个小结之外的个人体会
多 Agent 项目做到后面,我最大的体会是设计难度不完全在 Agent 数量上,而在“交接方式”上。你让两个 Agent 协作,不是把它们放进同一个群里就行,而是要明确它们各自看到的输入是什么、输出交给谁、失败怎么兜底。把 SubAgent 当成一种 Tool 来抽象,恰好能逼着你想清楚这些问题,因为工具接口不允许你含糊。
MAF 在这个方向上给了相当完整的基础设施,但它不会替你思考哪些任务适合拆出去。以我现在的判断标准来看,只有满足这几点的任务才值得拆成 SubAgent:子任务的提示词明显不同、子任务的输入输出边界清晰、子任务可以被独立测试。不符合这三点,硬拆只会增加复杂度和成本。
如果你刚开始接触这套东西,建议别一上来就上七八个 Agent。先拿一个主 Agent 加两个 SubAgent 练手,比如写代码加审查,或者搜索加总结。等你习惯了“任务如何拆分、结果如何回流、上下文如何隔离”这套节奏后,再慢慢往里面加角色。最后建议多留一部分精力在日志和可观测性上,多 Agent 系统一旦跑起来,传统的单线程排查思路会很快失效。
