我最初接触"agency-agents"这类多Agent协作框架时,其实是被逼出来的。单Agent方案在真实任务面前频繁掉链子,要么顾此失彼,要么一条路走到黑,我才意识到问题根源不在模型能力,而在任务拆解与角色协作的架构层。这个"agency-agents"项目正是围绕多智能体协作编排展开的一套实践方案,它解决的问题很具体:当任务需要多个能力角色配合时,如何定义角色、如何通信、如何编排流程、如何控制成本和稳定性。这篇文章我会把从零搭建多Agent协作系统的完整过程、核心设计考量、以及实际运行中踩过的坑一次讲清楚,适合已经玩过单Agent、正准备往多Agent迁移的开发者参考。
1. 为什么我会从单Agent转向多Agent协作
1.1 单Agent的瓶颈:一个"全能选手"什么都干不好
如果你只用过一个Agent处理任务,一定遇到过这种场景:你让它写一份市场分析报告,它先搜索资料,再写文案,最后还试图做数据图表。看起来一气呵成,但实际上每个环节的质量都差强人意。原因很简单——一个Agent内部的上下文窗口是有限的,角色定位是单一的。让同一个模型既当研究员又当策划又当编辑,它会在角色切换之间丢失重点。
我最早尝试用单Agent做"竞品分析日报"任务时,让它在一次对话里完成:搜索竞品动态、归纳核心变化、输出结构化报告。结果经常是搜资料搜到一半就开始写结论,或者把上个月的旧数据当成最新动态。模型并不是不聪明,而是同时承担的职责太多,注意力被稀释了。
换成多Agent协作之后,情况明显不一样:一个Agent专门负责搜索和筛选信息,一个Agent负责从原始素材里提炼要点,还有一个Agent负责格式化输出和检查。每个Agent只做一件事,任务边界清晰,模型对"自己当前该干什么"的理解就准确得多。
1.2 这个协作框架想解决的三件事
"agency-agents"这个项目方向,核心要回答三个问题:
- 角色如何定义与管理:每个Agent的职责边界、系统提示词、能力范围怎么配置,而不是简单地把一段prompt塞给模型。
- 任务如何拆解与编排:一个复杂任务应该被拆成几个子任务,谁先做谁后做,结果往哪里传递。
- 失败如何发现与恢复:多Agent协作最怕的就是某个环节悄悄出错,后续Agent还在错误结果上继续加工,导致系统性偏差。
这三点和传统软件工程里的模块化、服务编排、异常处理是一一对应的。很多人在多Agent项目里迷路,是因为他们把Agent当成"更强的模型"而不是"独立运行的执行单元"。相清楚了这一点,后面怎么设计结构都有方向。
2. 多Agent协作框架的核心设计问题
2.1 角色不是写个prompt那么简单
很多文章教你给Agent写"角色提示词"——"你是一个资深市场分析师"——但这远远不够。真正的角色定义至少需要四个维度:
一是职责边界:这个Agent负责什么、不负责什么。比如"研究员Agent只输出事实性摘要,不做市场判断",把边界写死,防止它越权去干别的活。
二是输入输出格式:它接收什么结构的数据、输出什么结构的数据。没有这个约束,Agent之间传消息就会变成"自由文本对话",解析起来极其痛苦。
三是工具权限:它可以使用哪些工具、不能使用哪些工具。研究员Agent可以调搜索工具,但不应拥有发送消息的权限;写作Agent可以调用模板工具,但不应直接访问原始数据。
四是退出条件:它什么时候算完成任务、可以交出控制权。我遇到过Agent在子任务里反复自我优化、迟迟不返回结果的情况,后来强制给每个角色定义了明确的终止条件。
把这四个维度固化下来,就是一份结构化的角色配置,而不是一句"你是一个XX专家"。在实践里这是多Agent系统稳定性最重要的设计决定,没有之一。
2.2 Agent之间到底怎么通信
多Agent协作系统里,消息协议的设计决定了系统的灵活性和排查难度。我用过的方案里,最省心的不是让Agent自由聊天,而是定义一套简单的结构化消息类型。
消息类型只需要五种就够覆盖绝大多数场景:
- task消息:调度器派发给某个Agent的指令,包含任务描述、输入数据、期望的输出格式。
- result消息:Agent完成任务后的返回值,必须符合任务要求的格式。
- error消息:Agent执行失败时返回的错误信息,包含失败原因和建议的重试策略。
- broadcast消息:需要通知所有Agent的信息,比如"全局上下文已更新"。
- control消息:停止信号、暂停信号、优先级调整信号。
这五种类型看起来简单,但能避免一个致命问题:Agent把运行时信息和业务数据混在一起。你如果不做类型区分,排查问题的时候根本分不清哪些消息是"指令"、哪些是"数据"、哪些是"状态同步"。
另一个通信设计要点是消息的追溯链路。每一条消息都要带一个任务ID和父任务ID,这样即使系统里同时跑几十个Agent,出问题时也能顺着消息链定位到具体是哪个环节。这个思路和分布式追踪的思路完全一致,直接用就行。
2.3 三种编排模式的实际适用场景
多Agent的编排方式没有标准答案,但我实际用下来发现,大部分场景都可以归入三种模式:
顺序管道模式:A做完传给B,B做完传给C。适合有明确处理链路的任务,比如"采集数据 -> 清洗数据 -> 生成图表 -> 撰写解读"。优点是逻辑简单、好排查;缺点是慢,所有环节串行。
广播汇总模式:一个主Agent把任务同时派给多个并行Agent,然后汇总各自结果。适合信息收集类任务,比如"从四个不同渠道分别了解某产品口碑,最后综合成一份报告"。优点是快,缺点是结果之间可能有重复和冲突,需要额外的归并逻辑。
超级管理员模式:一个主导Agent负责拆解任务、分派给多个专业Agent、检查结果、决定是否打回重做。适合没有固定流程、需要动态决策的复杂任务,比如"做一个开题方案,先调研再分析再写作,步骤不固定需要随机应变"。优点是最灵活,缺点是主导Agent的prompt设计难度很高,判断力是瓶颈。
实际项目里这三种模式常常混合使用,内部用管道模式处理固定链路,外层用超级管理员模式做顶层调度。关键是不管用哪种,都要先把任务流转图画出来再编码,不要一边写代码一边想流程。
3. 搭建一个多Agent协作小队的实操记录
3.1 用九行配置定义一个市场分析小组
说再多理论,不如直接看配置。我目前在用的一个"市场分析小队",由三个角色组成:信息采集员、数据分析师、报告撰稿人。信息采集员负责用搜索工具收集原始资料,数据分析师从资料里提取关键趋势,报告撰稿人把分析结果整理成可发布的文档。
每个角色的配置长这样:
[agent.researcher] name = "信息采集员" description = "负责搜索、筛选与任务相关的公开信息,只输出事实摘要" tools = ["web_search", "fetch_url"] input_schema = "research_request.json" output_schema = "research_result.json" max_iterations = 8 terminate_on = "output_schema_valid" [agent.analyst] name = "数据分析师" description = "从事实摘要中识别趋势、计算变化率,输出结构化分析结论" tools = ["calc", "statistics"] input_schema = "research_result.json" output_schema = "analysis_result.json" max_iterations = 5 terminate_on = "output_schema_valid" [agent.writer] name = "报告撰稿人" description = "根据分析结论生成面向指定受众的完整报告,检查事实一致性" tools = ["template_render", "consistency_check"] input_schema = "analysis_result.json" output_schema = "final_report.json" max_iterations = 5 terminate_on = "output_schema_valid"这份配置文件的重点不在于那几个字段名,而在于每个Agent都绑定了输入输出schema。信息采集员永远不知道分析结果长什么样,它只负责输出事实摘要;分析师的输入严格限定在采集员输出的JSON结构里。数据字段对不上,调度器直接拦截,不会让错误继续往下传。
3.2 工具注册:给Agent配上能干活的"手"
模型本身不能调用外部能力,所有超过"文本生成"范围的操作都依赖工具。工具注册的粒度很讲究:
- 粒度太粗:一个"do_everything"工具,Agent调用后返回一堆杂乱数据,没法程序化解析。
- 粒度太细:一个"add_two_numbers"工具,Agent每次都要想该调哪个,推理开销大还容易选错。
我实际划分粒度参考的是"一个人类助理能否独立完成"这个标准。比如"web_search"是一个工具,"fetch_url_detail"是另一个工具,但它们之间有调用先后关系;与其让Agent管理这个顺序,不如直接封装一个"research_topic"组合工具,内部自动完成搜索、抓取、去重、摘要四步。
工具返回的数据也要做结构化处理。不要让工具返回长文本让Agent自己去"看",而是让工具把结果整理成清晰的字段。比如搜索工具返回:
{ "status": "success", "total_found": 12, "items": [ {"title": "某产品3月销量", "source": "行业周报", "date": "2025-03-20", "summary": "...", "url": "..."} ] }Agent拿到这个结构,直接就能判断哪些条目值得采用,而不是把时间花在阅读一堆原始链接上。
3.3 执行循环:调度器怎么跑起来
搭建好角色和工具之后,真正驱动整个系统运转的是一个执行循环。我用伪代码描述一下最简版本:
def run_pipeline(task, agents): context = {"task": task, "artifacts": {}} for agent in agents: request = build_request(agent, context) result = dispatch(agent, request) # 调用LLM+工具 if not validate(result, agent.output_schema): result = retry_with_feedback(agent, request, result) context["artifacts"][agent.name] = result if agent.failed: return {"status": "failed", "failed_agent": agent.name, "error": result.error} return {"status": "completed", "artifacts": context["artifacts"]}这个循环有三个值得注意的细节:
- validate放在dispatch之后、放行之前:输出不合schema就跑重试逻辑,不进入下一个环节。
- 每个Agent独立记账:token预算、调用次数、耗时都有独立计数器,哪个Agent是吞金兽一目了然。
- 重试反馈要结构化:告诉Agent"你的输出缺少release_date字段,请补充后重新生成",而不是简单说"输出格式错误"。
这样一个简单循环就能支撑大部分顺序管道场景。如果要跑广播汇总模式或超级管理员模式,只需要在循环外面再加一个调度策略,内部每个子任务仍然走同样的执行逻辑。
4. 稳定性与成本:多Agent项目里的关键工程细节
4.1 预算约束与递归深度限制
多Agent系统的成本失控,往往不是单次调用贵,而是Agent陷入无意义的循环迭代。我之前出现过一次惨案:某Agent在生成报告时反复自我检查,发现一个小异常就去重新生成全文,连续循环了三轮,单个任务烧掉了平时六个任务的钱。
解决方案是给每个Agent设置四个硬性限制:
- max_iterations:单次任务最多执行的推理轮数,超过则强制终止。
- max_tokens_per_task:最大输出token数,防止生成超长内容。
- max_tool_calls:最多调用工具的次数,限制探索行为。
- budget_per_task:以金额为单位的硬预算,逼近时降级为最简输出。
这里有个取舍:限制太严会导致Agent完不成任务,限制太松又容易浪费。我的策略是先按正常任务的1.5倍设置上限,跑一批任务看分布,再根据P95值收紧。预算本质上是给异常留活口,不是用来卡正常任务的。
4.2 上下文膨胀与压缩策略
多Agent协作最容易忽略的问题是上下文累积。每个Agent除了要接收自己的输入,往往还需要阅读前序Agent的输出。如果前序输出很长,比如研究报告正文有四千多字,传递给下一环节时,光是理解这些上下文就要烧掉大量token。
我的压缩策略分三步:
第一步,每个Agent的输出schema里就设计好"紧凑字段"。报告Agent完整版输出存到文件里,但只需要给下游Agent传一个两百字的"要点摘要"字段。
第二步,任务输入里提供"快照模式"。当下游Agent需要理解上游做了什么时,不是把整个结果贴过去,而是贴结构化摘要加文件路径。
第三步,设置上下文窗口的"有效期"。超过N轮的旧内容自动折叠成概要,只有当前环节需要的内容保持在活跃区。
这个方法非常有效。同一套多Agent流水线,压缩前一个完整任务大约消耗十二万token,压缩后降到四万左右,而任务结果质量几乎没有变化。
4.3 结构化输出比自然语言路由可靠得多
多Agent协作中最常见的设计错误是让Agent用自然语言来"商量"下一步该干什么。比如Agent A说"我觉得现在应该让Agent B来处理数据",然后调度器从这句话里解析出意图。听上去很智能,实际运行的时候能把你折磨疯——模型表述稍微变一下,解析器就匹配不上。
我在项目里彻底转向了结构化决策协议。调度器向Agent下发任务时,附带上选项列表:
{ "decision_request": true, "options": [ {"id": "continue", "description": "继续在当前环节追加工作"}, {"id": "handoff", "description": "把结果传给下一位Agent"}, {"id": "request_clarification", "description": "需要补充信息"} ], "required_fields": ["selected_option", "reason", "next_receiver"] }Agent不再输出"我觉得下一步应该……",而是直接返回结构化决策。一个小改动,把系统的判断准确率和可调试性都提高了不少。维护自然语言的Agent协作对话,约等于维护一个没人看得懂的隐式状态机,迟早要出问题。
5. 踩坑实录:多Agent协作最容易翻车的四个场景
5.1 死循环:两个Agent互踢皮球
这个坑我印象太深了。一个采集任务里,信息采集员发现某数据源访问失败,按照设定向调度器报告错误;调度器重新派发任务;采集员又去访问同一个失败的数据源;再报错;再派发。日志看起来两个角色在非常忙碌地工作,实际上是在原地打转。
问题的根源是:采集员的失败条件没有区分"暂时性失败"和"永久性失败"。数据源404是永久失败,重试一百次也是404;网络超时才是暂时性失败,重试才有意义。
修这个bug的方式很直接:在Agent的error消息里增加一个字段retryable,由工具层根据错误类型如实标注。调度器发现retryable为false时,直接跳过该任务并把异常记录到最终报告,不再反复重试。
5.2 集体幻觉:一个错了,全员跟着错
单Agent幻觉问题顶多是输出里编一段不存在的论断,多Agent系统里的幻觉会沿链条放大。我的一个分析任务里,采集员在搜索摘要时产生了一条与实际网页不符的"事实",后面分析师基于这条假事实做了深度分析,撰稿人又把分析写成了结论。最后整个报告看起来逻辑清晰,但根本是空中楼阁。
这暴露了一个架构缺陷:系统没有对上游事实做交叉验证。之后我做了两条防呆设计:
- 每个"事实型输出"必须带来源标识(url、日期、原文片段),没有来源的论断不允许进入下游。
- 增加一个独立的"事实核查员"角色,专门随机抽检最终报告中的来源断言是否真实存在。
成本增加了一些,但报告出错率大幅下降。对于面向对外发布的内容,这个校验环节不能省。
5.3 角色漂移:写着写着任务就串了
多Agent系统的角色边界不是天然稳定的。我遇到过这场景:信息采集员原本只负责输出事实摘要,某次任务里它居然开始对数据做趋势分析,还很贴心地给出了建议——这些明明该由分析师角色来做。
原因不难理解:任务描述里如果有"请帮我分析一下竞品动向"这种措辞,采集员会"乐于助人"地顺便把分析也做了。这种行为看似智能,实际破坏了系统的职责边界,让后续流程无法确定某段结论到底来自哪个角色、可靠度如何。
对策是在配置里加上严格的权限定语:"你只能输出你在工具中检索到的事实信息,任何超出事实归纳的推测一律禁止输出。如果任务要求你做推测,请回复error类型消息并注明超出职责范围。"加上这句之后,角色漂移问题几乎绝迹了。
5.4 Token超预算:日志居然吃掉了一半费用
最后一个坑很隐蔽,也是最亏的。某次我例行检查任务账单,发现一个单次任务花费奇高,打开详情一看——整整一半token开销花在了系统提示词和日志注入上。
多Agent框架往往会在每轮调用里拼接完整系统提示词、上下文摘要、前几轮的历史记录。历史记录里有工具调用的完整入参和返回值,而这些对于当前环节的Agent根本不需要。比如搜索工具的返回里包含十条长摘要,到了写报告的Agent那里,历史记录仍然背着这十条摘要,白白烧钱。
这个问题排掉之后,我对系统做了一次"瘦身":
- 每轮只保留当前Agent指尖需要的最少上下文。
- 工具调用的原始返回,默认不进历史记录,只保留经过提炼的摘要字段。
- 对日志级别做分层,调试信息只在debug模式下记录,线上默认记录关键决策点。
调整完之后,单任务花费降了接近四成。上下文管理不是理论问题,是真金白银的成本问题。
写到这里,我再分享一条贯穿始终的个人体会:多Agent协作项目里,80%的技术精力其实花在了工程约束上,而不是模型能力上。角色边界、消息协议、schema校验、预算控制,这些看起来枯燥的机制,恰恰是项目能稳定运行的根本。当初我要是早一点把这类框架的配置逻辑吃透,大概能少走一半弯路。如果你正要开始一个多Agent项目,建议先把角色和通信机制画清楚,再让模型进场干活,顺序别反了。