
最近在给一个内部交付项目搭多Agent架构需求很典型用户丢一段代码进来系统需要自动完成代码审查、生成单元测试、再整理成开发文档。第一版我想得很简单把所有指令塞进一个Agent的System Prompt让一个大模型一口气干完。结果跑了两个星期效果惨不忍睹代码审查和文档生成的指令互相污染上下文被撑到爆输出格式一天一个样。后来我换成Microsoft Agent Framework用SubAgent把每个职责拆开才真正把流程跑稳。这篇文章就把我的完整思路、踩坑过程以及可复现的代码写出来给同样在做Multi-Agent应用的开发者做个参考。如果你已经用过AutoGen、Semantic Kernel或LangChain想了解微软官方多Agent框架怎么做主从模式这篇文章应该能帮你节省不少时间。1. 为什么需要SubAgent从单Agent到多Agent的必然1.1 单Agent体系的瓶颈不是能力不够是职责太多我在项目一开始的做法很朴素把“代码审查员”、“测试工程师”、“文档写手”三个角色的要求全部写进同一个System Prompt让GPT-4o一次性输出三样东西。在大模型能力足够强的情况下这种方案确实能跑通但进入真实业务后问题一个接一个。第一个问题是上下文膨胀。一次完整的代码审查加测试生成加文档整理对话历史里既要保留原始代码又要保留审查意见还要保留中间讨论过程几轮下来就逼近上下文窗口上限。第二个问题是指令冲突审查代码时要“严格指出问题”写文档时要“语气友好、适当鼓励”当这些要求同时出现在同一个Prompt里模型经常把握不好优先级。第三个问题更致命——角色一旦多了输出格式就开始漂移今天先审查明天先写文档后续解析程序根本没法稳定处理。这是单Agent体系的结构性缺陷不是模型能力不够而是你试图让一个实体同时承担太多互斥职责。与其在大模型能力上做文章不如从架构上把职责拆开。SubAgent就是干这个的。1.2 主从模式更可控对等讨论适合研究主从结构适合交付多Agent设计大致有两条路线对等模式和主从模式。对等模式就是多个Agent放在一个GroupChat里自由发言类似几个专家开圆桌会议。这种模式在学术研究和头脑风暴场景里很有用思路发散、互相启发但放在生产环境里问题很多谁先发言什么时候结束有人跑题了谁来拉回来最终结果是否收敛这些都需要额外的机制来保证。主从模式则是另一种思路一个Orchestrator主控Agent负责理解用户需求、拆解任务、决定调用哪个SubAgent每个SubAgent只负责一个独立子任务做完把结果交回来。这就像公司里的项目经理带几个专员项目经理不亲自写代码但清楚每一步该找谁。我在实际项目里最终选了主从模式原因很实在可控。主从模式下每一步谁执行、执行什么、输出什么格式都能在设计阶段定下来出问题时也能快速定位是编排逻辑的问题还是某个SubAgent的问题。对等模式虽然灵活但对于一个要交付给客户的系统来说灵活性恰恰是风险的来源。1.3 换个角度看SubAgent本质上就是另一种Tool这里要重点说一个对我影响很深的设计思路从主Agent的视角看调用一个SubAgent和调用一个普通函数工具在调用流程上没有本质区别——都是“识别需求→选择工具→传入参数→拿到结果”。区别在于工具的内部实现普通Tool执行的是确定性代码输入输出有严格schema返回JSON速度快、成本低SubAgent背后是一个完整的LLM对话循环它能做推理、能自己决定怎么调用其他工具、返回的是自然语言和推理链。一个更直白的理解方式是如果你把普通的API调用看作“打电话给一个固定流程的客服系统”那SubAgent就是“打电话给一个能独立思考的专家”专家能听懂你的问题、能自己判断怎么办、能给你一个经过分析的答复。理解了这一点设计时就多了一把尺子这个子任务到底需不需要独立的判断和规划能力如果只是查数据库、算个分、调用外部API用Tool就够了省钱又快只有当任务需要在某个领域内深度思考、需要多轮规划、或者需要独立的专业领域提示词时才值得上一个SubAgent。把SubAgent当Tool来设计你的架构会简单很多。2. SubAgent有哪些类型怎么选2.1 按职责属性划分五种最常用的SubAgent在实际项目里把常见的SubAgent按职责分类大致有五种我一个个说清楚它们分别解决什么问题。领域专家型是最常见的一类。它只在一个垂直领域内工作比如代码审查专家、法务顾问、医疗知识问答助手。特点是你把某个领域的专业知识、判断标准全放进它的System Prompt它只处理这个领域内的问题。好处是领域知识集中存放不会和其他角色交叉污染。工具编排型是把多个工具串成一个完整流程对外只暴露一个对话式接口。例如你要做一个“市场调研报告生成”SubAgent它内部需要调用搜索工具、抓取网页工具、数据格式化工具但对外统一成一个Agent。调用方不需要关心内部流程只需要告诉它“查一下某某行业的市场规模”它自己编排后返回一份结构化报告。审核校验型承担的是“质检员”角色。主Agent或另一个SubAgent输出结果后由它检查是否符合规范比如输出格式对不对、有没有遗漏关键字段、答案是否自洽。这类Agent的System Prompt通常很短核心就是“你只负责挑毛病不要修改内容”。数据收集型负责多源信息获取和整合。比如要回答“近三年某某领域的论文趋势”它可以并行检索多个数据库把结果去重、清洗、汇总成一份结构化摘要。这类Agent通常需要配合检索组件使用重点是控制检索深度和返回信息的密度。递归分解型更像一个小项目经理。它收到一个大型任务后会先把任务拆成多个子任务然后对自己或其他Agent递归派发。这类Agent适合处理“写一份完整竞品分析报告”这种复杂度较高的任务需要拆成资料收集、数据分析、报告撰写等多个子步骤。2.2 按触发方式划分静态路由、动态路由和混合式从“什么时候调这个SubAgent”的角度又可以把SubAgent分为三种触发方式。静态路由指调用顺序是写死的。比如我项目里的固定流程代码审查→测试生成→文档整理这个顺序不会变所以用静态路由最合适。这种方式的优点是好调试、好预测缺点是不够灵活用户如果只想做代码审查也得走完整个流程。动态路由指主Agent根据用户输入自己决定下一步调哪个SubAgent。比如一个客服系统里有订单查询、退换货办理、投诉建议三个SubAgent用户具体提哪种需求由主Agent根据语义判断后路由。这种方式的优点是灵活、用户体验好缺点是主Agent的判断可能有误差需要精心设计路由规则和纠错机制。混合式比较适合复杂场景主流程固定但主流程内部的某些环节需要动态选择。比如先固定做代码审查审查结果出来后根据问题严重级别可能需要生成测试、可能需要修复建议、也可能直接结束。这个“根据结果决定下一步”的逻辑就是动态的。我自己的经验是能用静态路由解决的场景不要上动态路由。动态路由对主Agent的指令遵循能力要求很高一旦判断错整个流程都会偏。2.3 一张表帮你决定用SubAgent还是用普通Tool很多朋友问我说“我有了Agent Framework是不是所有子任务都应该做成SubAgent”我的答案是恰恰不是。能用Tool解决的绝对不要上SubAgent这个决定可以用一张表来辅助判断。判断维度用普通Tool用SubAgent任务是否单轮可完成是一次调用即返回否需要多轮推理是否需要领域专业判断不需要逻辑固定可写死需要依赖领域知识和经验输入输出是否结构化是字段明确否输入开放、输出需理解是否需要独立规划能力不需要需要成本敏感度每次调用微乎其微每次调用消耗完整LLM对话任务流程是否稳定不变稳定经常变化或需要自适应总结下来一句话如果这个子任务不需要“思考”就用Tool如果它需要“思考”才上SubAgent。我在这个项目里一开始把三个角色都做成SubAgent后来发现测试生成这一步其实用模板加规则也能做换上普通Tool后成本立刻降了60%以上响应速度也快了很多。3. 在Microsoft Agent Framework里动手搭一个SubAgent3.1 环境准备与项目骨架我用Python做示例因为Python在AI开发里最通用框架本身也支持.NET理念是一致的。先把框架装好pip install microsoft-agent-framework项目结构我建议按Agent职责分包方便后期维护agent_demo/ ├── main.py # 入口负责初始化框架并启动GroupChat ├── agents/ │ ├── __init__.py │ ├── code_reviewer.py # 代码审查SubAgent │ ├── test_generator.py # 测试生成SubAgent │ └── doc_writer.py # 文档整理SubAgent ├── orchestration/ │ ├── __init__.py │ └── chat_manager.py # 自定义的编排管理器 ├── .env # 模型配置 └── requirements.txt模型配置我放在环境变量里管理避免把密钥写死在代码中OPENAI_API_KEYyour_key OPENAI_API_BASEhttps://api.openai.com/v1 MODEL_NAMEgpt-4o3.2 定义Worker SubAgent让它专精一个方向框架里创建一个SubAgent很简单核心就是ChatCompletionAgent。我给它起一个明确的名字配置模型然后写一段职责清晰的System Prompt。以代码审查Agent为例# agents/code_reviewer.py import os from microsoft.agent import ChatCompletionAgent, ModelConfig model_config ModelConfig( modelos.getenv(MODEL_NAME, gpt-4o), api_keyos.environ[OPENAI_API_KEY], api_baseos.environ.get(OPENAI_API_BASE), ) code_reviewer ChatCompletionAgent( nameCodeReviewer, model_configmodel_config, instructions( 你是一名资深代码审查专家只负责代码质量审查。\n 接收输入一段源代码。\n 审查维度可读性、潜在Bug、安全隐患、性能问题。\n 你必须严格按下面的JSON格式输出不要输出任何其他内容\n {\issues\: [{\severity\: \high|medium|low\, \location\: \问题位置\, \description\: \问题描述\}]}\n 如果代码没有明显问题输出 {\issues\: []}。\n 如果输入不是代码输出 {\error\: \input_is_not_code\}。 ) )关键点有几个第一System Prompt里明确写了“只负责代码质量审查”这是边界约束防止SubAgent越权处理其他请求第二输出格式强制为JSON并给出了具体schema这样下游程序解析起来非常干净第三对异常输入做了明确定义这为后面的错误处理打好了基础。测试生成Agent和文档整理Agent的写法完全一样只是换名字和Prompt。测试生成Agent的Prompt是“根据代码和审查意见生成单元测试”文档整理Agent是“根据代码、审查意见和测试结果生成一份面向开发者的说明文档”。三个Agent切出来后各自的Prompt都很短职责非常清晰。3.3 用AgentGroupChat把主从结构串起来Microsoft Agent Framework里把多个Agent聚在一起用的是AgentGroupChat。如果流程是固定的顺序执行框架直接提供了一个SequentialAgentChatManager按Agent列表的顺序依次触发# main.py import asyncio from microsoft.agent import AgentGroupChat, SequentialAgentChatManager from agents.code_reviewer import code_reviewer from agents.test_generator import test_generator from agents.doc_writer import doc_writer group_chat AgentGroupChat( agents[code_reviewer, test_generator, doc_writer], chat_managerSequentialAgentChatManager(), ) async def main(): result await group_chat.run( user_input请帮我处理下面这段Python代码\npm MongoClient()\nusers pm.users.find_one() ) print(result) if __name__ __main__: asyncio.run(main())这个方式特别适合流程固定的场景三个Agent顺序执行前一个Agent的输出会作为后一个Agent输入的一部分。第一次跑通整个流程时我就发现代码审查Agent输出的JSON被测试生成Agent很好地利用上了说明框架的消息传递机制是自动衔接的。不过这里要提醒一点SequentialAgentChatManager的顺序是你在agents参数里指定的顺序不是字母顺序也不是模型自动决定的顺序。所以你要仔细想清楚流程顺序再填这个列表我一开始把文档整理Agent放在第二个位置结果文档先被生成测试用例反而没了。3.4 主从模式的核心自定义ChatManager做编排只用顺序执行当然不够真正的业务里常常需要主Agent做决策决定下一步派谁干活。这时就要自定义ChatManager。我的做法是一个Orchestrator Agent负责规划自定义Manager负责根据Orchestrator的输出路由到指定的Worker。# orchestration/chat_manager.py from microsoft.agent import Agent, AgentGroupChat, ChatManager class OrchestratorChatManager(ChatManager): def __init__(self, orchestrator: Agent, workers: dict): self.orchestrator orchestrator self.workers workers # {code_review: Agent, test_gen: Agent, doc_write: Agent} self.pending_worker None self.started False async def get_next_agent(self, chat_history: list) - Agent | None: if not self.started: self.started True return self.orchestrator # 解析orchestrator的最终输出决定派给哪个SubAgent last_message chat_history[-1] if last_message.sender self.orchestrator.name: plan parse_plan(last_message.content) # 从JSON中读取 {next_worker: code_review, ...} self.pending_worker self.workers.get(plan.get(next_worker)) return self.pending_worker if last_message.sender self.pending_worker.name: # 如果worker执行完需要再次回到orchestrator判断下一步 self.pending_worker None return self.orchestrator return None这个Manager的核心逻辑是先让Orchestrator发言然后根据Orchestrator输出的JSON里的next_worker字段路由到指定SubAgentSubAgent执行完再回到Orchestrator由它决定是继续派下一个任务还是结束。这里有一个设计体会Orchestrator的输出必须结构化。我在Orchestrator的System Prompt里明确要求它输出固定JSON格式{reasoning: 简要的决策理由, next_worker: code_review|test_gen|doc_write|finish, input_for_worker: 传给下个Agent的内容}。有了这个约束路由逻辑就是一段简单的JSON解析不需要让代码去理解自然语言可靠性高得多。3.5 消息传递、上下文管理和结果回传多Agent系统里最容易翻车的点是消息传递每个Agent该看到什么、不该看到什么。AgentGroupChat内部会维护一个共享的chat_history默认情况下所有Agent都能看到全部历史。这在Agent少的时候问题不大但SubAgent多了以后就会出现“下游Agent被大量无关历史干扰”的情况。我采用的做法是只传必需信息。每个SubAgent的输入不是整个对话历史而是由Orchestrator从历史中抽取并重组后的“任务单”。比如测试生成Agent拿到的是代码片段和代码审查结果而不是用户和Orchestrator之间所有的寒暄和上下文。这个可以放在Orchestrator的指令里让它完成信息筛选也可以在自定义Manager里将发送给SubAgent的消息内容做裁剪。另外一个实用技巧是用摘要代替完整历史。当任务链很长时我会在Orchestrator里加一道指令“在每次派发任务前用三句话概括一下当前任务状态把概况作为worker的输入”。这样SubAgent不用读全部历史拿到摘要就能干活token消耗大幅下降。结果回传的方向同样重要。我在设计里明确要求所有SubAgent的输出都必须是结构化结果这样Orchestrator拿到结果后可以立即做决策。如果某个SubAgent返回的不是预期格式Manager里会捕获解析异常并走重试逻辑而不是让错误信息继续在Agent之间传播。4. 让SubAgent体系稳定运行的几个关键细节4.1 System Prompt就是岗位说明书写不好就等着串戏多Agent系统里SubAgent的System Prompt比单Agent系统更关键因为它同时承担着两个职责对外描述“你能做什么”对内约束“你不能做什么”。我见过很多项目栽在Prompt写得不够具体上SubAgent之间互相越权、抢任务、甚至把别的Agent的指令当成用户输入来执行。我给SubAgent写Prompt时固定用四段式结构角色定位、职责边界、输入约定、输出格式。角色定位一句话说清楚你是谁职责边界明确说你不做什么输入约定说明你会收到什么格式的数据输出格式给出严格的JSON schema和示例。尤其是输出格式我建议直接给一个“正确示例”加一个“错误示例”模型对示例的理解远比对文字描述更准确。另一个技巧是在Prompt里明确告诉SubAgent“你的输出会被下一个Agent解析”这个概念一旦建立模型就会自动倾向于输出更结构化、更清晰的内容。你可以理解为当员工知道自己的报告要被别人审核时写的东西自然会更规范。4.2 上下文管理不控制historytoken账单会教你做人Multi-Agent系统的上下文膨胀速度比单Agent快得多因为每多一个Agent“共享历史”就要复制给每一个Agent。一旦一个GroupChat里有四五个Agent每轮对话的token消耗就是单Agent的好几倍。我这边做了三件事来控制成本。第一限制GroupChat的终止条件设置最大轮数比如MaxTermination(total8)超过轮数强制结束防止Agent之间无限讨论下去。第二在Manager层做消息裁剪发给特定SubAgent的消息只保留它需要的部分而不是整段历史。第三对大段上下文做摘要用压缩后的摘要替代完整的中间对话。实践中还有一个容易被忽略的地方单个Agent内部的多轮对话。如果你的SubAgent在内部调了多次LLM来完成一个任务这部分的token消耗也要纳入预算。我通常把这种“内部循环”限制在最多两轮超过就返回部分结果宁可结果不完整也不让单次任务成本失控。4.3 错误处理每个SubAgent都要有“失败预案”单Agent系统出错最多就是一次任务失败多Agent系统出错错误会像滚雪球一样在Agent之间传递放大。一个SubAgent返回了非预期格式下游Agent拿着错误数据继续推理最后的输出可能离题万里而且排查起来非常痛苦。所以我在设计每个SubAgent时都会约定一套错误返回格式。比如前面定义的CodeReviewer如果输入不是代码就返回{error: input_is_not_code}。Manager收到这个error字段后可以决定是重新派发、换一个Agent还是直接把错误返回给用户。这个逻辑相当于给每个SubAgent配了一个“失败预案”。另外超时和重试机制一定要在框架外部做一层保护。我习惯给每个Agent调用包一层超时控制超过30秒强制中断中断后重试一次如果还是失败就记录日志并降级为普通Tool调用确保整个流程不会因为单个SubAgent卡住而全部瘫痪。4.4 可观测性多Agent排障必备的日志设计多Agent系统的调试难度是单Agent的平方。为了能定位问题我在项目里加了三层观测。第一层给每次用户请求生成一个request_id所有Agent的日志都带上这个id这样可以串起一整个调用链。第二层在每个Agent的输入输出位置打印摘要日志包括发送方、接收方、消息前200个字符、token消耗这样能很快看出是哪一步出了问题。第三层记录每个SubAgent的调用次数和每次的token数连续跑几天就能看到各个SubAgent的成本分布哪些环节成本超标一眼就知道。日志打太多会刷屏我的经验是摘要日志必须打只要截断过长的内容完整内容打到单独的debug文件里按request_id命名出问题时再去翻细节。这样的分层日志设计既能保证日常开发时的可读性又能保留排障所需的完整信息。提示多Agent系统最怕“一切正常但结果不对”的状况。有了request_id串联的日志你可以一步步回放每个Agent的决策过程快速定位是哪个SubAgent给出了错误判断。5. 常见问题与排查实录5.1 高频问题速查表症状可能原因解决办法GroupChat执行到一半就停了没有配置终止条件或Manager返回了None配置MaxTermination检查get_next_agent的返回逻辑SubAgent之间互相抢任务使用了对等模式且Prompt边界不清换成主从模式强化每个Agent的职责边界对话陷入无限循环缺少终止条件或Orchestrator反复派发相同任务设置最大轮数在Orchestrator指令里增加“不许重复派发已完成任务”token消耗异常高历史消息全量传给每个Agent消息裁剪只传当前SubAgent需要的片段对历史做摘要下游Agent拿到了解析不了的输出上游Agent的输出格式不符合预期强制JSON输出并在Prompt中给出示例Manager层加格式校验主Agent路由决策错误动态路由规则不够明确给Orchestrator定义清晰的JSON决策格式减少自由发挥空间单个SubAgent响应超时任务过重或内部多轮循环过多限制内部循环轮数设置超时和重试机制5.2 三个真实踩坑案例第一个坑是GroupChat不加终止条件。我最初用对等模式跑了4个Agent做一次头脑风暴测试结果几个Agent聊嗨了互相补充、感谢、再补充几分钟跑了几万token。这个问题后来靠两个手段解决一是强制设置最大轮数二是给每个Agent一个“结束权限”当它觉得任务已完成时可以输出结束标记。在生产环境我强烈建议任何一个多Agent系统都加上强制轮数上限哪怕你觉得业务场景不会触发也要防一个万一。第二个坑是把源代码全量塞给每个SubAgent。早期设计里为了让三个SubAgent都有足够背景我把完整的用户代码发给每一个Agent。结果代码长一点内存和token一起涨而且下游Agent被无关细节干扰输出质量反而下降。后来我改成“摘要任务单”模式每个SubAgent只拿到自己干活需要的那部分数据比如文档整理Agent拿到的是代码的核心逻辑摘要和审查结果而不是整段源码。改完之后质量和成本都改善了。第三个坑是格式校验缺失。有一版测试生成Agent偶尔会返回Markdown格式而不是JSON下游文档整理Agent拿到后直接报错整个流程失败。我在Manager层加了一个JSON解析校验解析失败就自动带上前一轮的原始输入重试一次如果还失败就降级调用一个写死的模板。加上这层保险之后系统稳定性明显提升也让我意识到多Agent系统里任何一步都不能假设上游一定给的是完全正确的结构。写在最后一个关于SubAgent的个人经验做这个项目的过程中我最大的体会是在真实项目里一定要克制新增SubAgent的冲动。多Agent系统每多一个SubAgent就多了一个需要维护的System Prompt、多了一份额外的token消耗、多了一个出问题的环节。我在项目二期有过一个想法想再加一个“代码优化建议”SubAgent后来仔细评估发现这个任务用普通Tool加几组规则就能覆盖80%的场景最终没有加SubAgent。实际跑下来系统的稳定性和成本控制的确比初期好很多。最后再分享一个实用小技巧给SubAgent写System Prompt时先写“你不做什么”再写“你要做什么”。负向约束能明显减少串戏现象这个经验我在多个项目里都验证过。如果你正准备用Microsoft Agent Framework搭自己的Multi-Agent系统记住这个原则你会少踩很多坑。