资讯动态

agency-agents:多智能体协作框架的设计与实现

发布时间:2026/10/25 3:36:48 来源:尧图企业网站定制
1. 从“agency-agents”这个名字说起它到底想解决什么问题第一次看到agency-agents这个项目名很多人会愣一下——agency 和 agents 两个词叠在一起读起来像是某种“代理的代理”。我最初也是这个反应。但如果你在自动化、任务编排或者多智能体协作这个圈子里待过一段时间就会意识到这个名字其实相当精准它指向的是一类让多个智能体agents像一家代理机构agency那样协同工作的系统框架。说白了agency-agents要处理的核心矛盾是单个智能体再强也有能力边界。你让它写代码它可能不擅长做视觉审查你让它做数据分析它可能对文案一窍不通。而现实中的任务往往是复合型的——比如“帮我调研一下某个技术方案写一份对比报告再生成一份演示文稿”。这种任务拆开来看至少涉及信息检索、逻辑归纳、文案撰写、格式排版四个环节。如果全压给一个 agent结果往往是前面还行后面就开始胡言乱语。agency-agents的思路就是把一个大任务拆成若干子任务每个子任务交给一个专门化的 agent再由一个协调层orchestrator来管理它们之间的依赖关系、数据流转和结果汇总。这听起来和传统的 workflow 引擎有点像但关键区别在于——这里的每个节点不是死板的函数而是具备一定自主决策能力的智能体。它们可以自己判断“这个信息够不够”“要不要再查一轮”“输出格式对不对”而不是完全依赖预设的 if-else 分支。这个项目适合谁来研究我认为有三类人值得花时间看看第一类是做自动化工具链的开发者手头有一堆零散的脚本和 API 调用想升级成更灵活的智能编排第二类是做 AI 应用的产品或技术负责人正在头疼“单 agent 效果不稳定”的问题第三类是对多智能体系统感兴趣的学习者想找一个结构清晰、可拆解、可复现的参考实现。不管你属于哪一类理解agency-agents的设计逻辑比单纯跑通它的 demo 更有价值。2. 拆开看骨架agency-agents 的四个核心抽象2.1 Agent 不是“更聪明的函数”而是有状态的执行单元在很多人的直觉里agent 就是一个“输入 prompt、输出结果”的黑盒。但agency-agents对 agent 的定义要重得多。一个 agent 在这个框架里至少包含四样东西角色描述role、能力边界capability、工具集tools和记忆memory。角色描述决定了它“以什么身份说话”。比如一个负责代码审查的 agent它的角色描述里会强调“关注边界条件、异常处理、命名规范”而不是泛泛的“你是一个有用的助手”。能力边界则明确它“能做什么、不能做什么”——这很重要因为如果没有边界agent 会倾向于对所有任务都给出一个“看起来像那么回事”的回答而不是老实说“这个我不擅长建议转给其他 agent”。工具集是 agent 和外部世界交互的通道。一个检索 agent 需要搜索工具一个代码 agent 需要执行环境一个文档 agent 需要文件读写能力。agency-agents的设计里工具是挂在 agent 实例上的而不是全局共享的。这样做的好处是隔离性好——检索 agent 不会误调用代码执行工具减少了意外副作用。记忆则是 agent 的“上下文延续能力”。没有记忆的 agent 每次调用都是“失忆”状态多轮协作时就会反复问同样的问题。agency-agents给每个 agent 维护了一个轻量级的短期记忆通常以消息列表的形式存在配合一个可选的长期记忆存储比如向量库来跨会话保留关键信息。注意很多初学者会跳过“能力边界”这一步觉得写不写无所谓。实际跑起来就会发现没有边界的 agent 在遇到超出能力范围的任务时会强行编造一个答案而不是触发转交逻辑。这个坑我踩过不止一次。2.2 Orchestrator任务分发的“调度台”而不是“大脑”agency-agents里最容易被误解的组件就是 orchestrator。有人把它当成“总指挥”觉得所有决策都应该由它来做。但更准确的理解是orchestrator 是一个调度台它负责的是“谁来做、什么时候做、做完之后给谁”而不是“具体怎么做”。这个区分很关键。如果 orchestrator 试图理解每个子任务的细节它就会变成一个臃肿的“超级 agent”失去了拆分的意义。agency-agents的做法是orchestrator 只维护一张任务依赖图DAG每个节点是一个 agent 调用边表示数据依赖。它根据依赖关系决定执行顺序把上游 agent 的输出打包成下游 agent 的输入然后等待结果。具体来说orchestrator 的工作流程大致是这样的接收顶层任务描述调用一个“规划 agent”把任务拆成子任务列表。根据每个子任务的性质从注册表中匹配最合适的 agent。构建依赖图检测是否存在循环依赖或孤立节点。按拓扑顺序执行遇到并行分支时并发调用。收集所有叶子节点的输出调用一个“汇总 agent”生成最终结果。这里有个设计细节值得注意规划 agent 和汇总 agent 本身也是 agent而不是硬编码的逻辑。这意味着你可以替换不同的规划策略——有的规划 agent 倾向于拆得细有的倾向于拆得粗有的汇总 agent 擅长写报告有的擅长生成结构化数据。这种“用 agent 管理 agent”的递归结构是agency-agents灵活性的来源。2.3 消息总线agent 之间怎么“说话”多个 agent 协作最大的工程问题不是“谁聪明”而是“怎么把数据从 A 传到 B 而不丢不重不乱”。agency-agents采用的是一个轻量级的消息总线机制每个 agent 调用产生的结果会被封装成一条消息消息里包含发送者 ID、接收者 ID、任务 ID、载荷payload、时间戳和状态标记。这个设计看起来朴素但解决了一个很实际的问题当某个 agent 执行失败时orchestrator 可以根据消息的状态标记决定是重试、跳过还是回滚。如果没有这层消息抽象直接传对象引用一旦某个 agent 修改了共享数据排查问题会非常痛苦。消息总线的另一个好处是可观测性。你可以把每条消息记录下来形成一个完整的执行轨迹。调试的时候打开轨迹一看就知道是哪个 agent 在哪个环节出了问题而不是面对一个“最终结果不对”的黑盒干瞪眼。2.4 注册表与发现机制agent 从哪来agency-agents里有一个 agent 注册表registry本质上是一个字典结构key 是 agent 的名称或能力标签value 是 agent 的工厂函数或实例。orchestrator 在匹配子任务时就是通过查询这个注册表来找到合适的 agent。注册表的设计有两种常见模式静态注册和动态发现。静态注册就是在启动时把所有 agent 都注册好简单直接适合 agent 数量不多、类型固定的场景。动态发现则是运行时根据配置文件或远程服务来加载 agent适合需要热插拔或分布式部署的场景。agency-agents默认走的是静态注册路线但预留了动态加载的接口。我的建议是如果你只是做原型验证静态注册足够了如果你打算把它做成一个长期维护的平台尽早考虑动态发现否则每次加一个新 agent 都要改启动代码很快就会烦。3. 为什么不是“一个大模型全包”拆分背后的工程逻辑3.1 上下文窗口的硬约束很多人会问现在大模型的上下文窗口越来越大为什么还要拆成多个 agent直接把所有信息塞给一个模型不就行了这个问题问得好但答案也很直接上下文窗口再大也有两个绕不过去的限制。第一是成本——你把十万 token 塞进去每次调用都按十万 token 计费多轮下来账单会很可观。第二是注意力衰减——模型对长上下文中间部分的关注度天然低于开头和结尾这是目前架构层面的特性不是靠“提示词写得好”就能完全解决的。agency-agents的拆分策略本质上是一种上下文管理手段。每个 agent 只接收和它任务相关的信息处理完之后输出一个精炼的结果而不是把原始的全量上下文继续往下传。这样每一跳的上下文都是可控的既省钱又稳定。3.2 专业化带来的质量提升我做过一个对比实验同一个“调研写报告”的任务用单 agent 跑十次再用agency-agents拆成“检索 agent 归纳 agent 撰写 agent”跑十次。单 agent 的结果里大约有三次会出现“信息堆砌但缺乏逻辑主线”的问题而拆分后的结果结构清晰度明显更高。原因不难理解当一个 agent 只需要做“归纳”时它的提示词可以写得很聚焦——“从以下材料中提取三个核心观点每个观点用一句话概括并标注出处”。这种聚焦的指令比“帮我写一份调研报告”这种宽泛指令执行起来确定性高得多。3.3 失败隔离与可恢复性单 agent 执行长任务时如果中间某一步出错往往需要从头再来。而agency-agents的 DAG 结构天然支持失败隔离如果“撰写 agent”失败了前面的“检索”和“归纳”结果还在只需要重跑撰写环节即可。这个特性在实际使用中价值很大。尤其是当任务涉及外部 API 调用时比如搜索、数据库查询重跑整个流程意味着重复计费和重复等待。有了失败隔离你可以只重试出错的那一个节点。提示要实现真正的失败隔离需要确保每个 agent 的输出是幂等的、可序列化的。如果某个 agent 的输出依赖运行时状态比如临时文件路径重试时可能会出问题。建议所有 agent 的输出都走消息总线不要直接操作共享文件系统。4. 从零搭一个最小可用的 agency-agents 原型4.1 环境准备与依赖选择要跑通一个最小原型你不需要一上来就搞分布式。一台普通开发机Python 3.10加上几个基础库就够了。核心依赖包括一个 HTTP 客户端用来调用模型 API、一个轻量级的数据校验库用来定义消息结构、一个图处理库用来管理 DAG。我的建议是不要一开始就引入重量级的工作流引擎。很多人习惯性地想上 Airflow 或者 Prefect但那些工具是为数据管道设计的和 agent 编排的语义模型不完全匹配。agency-agents的 DAG 通常只有几十个节点自己用字典和列表实现拓扑排序完全够用而且更可控。4.2 定义消息结构一切从契约开始在写任何 agent 逻辑之前先把消息结构定下来。这是整个系统最基础也最重要的部分。一个典型的 agent 消息可以这样定义from dataclasses import dataclass, field from typing import Any, Optional from datetime import datetime dataclass class AgentMessage: msg_id: str sender: str receiver: str task_id: str payload: Any status: str pending # pending / running / success / failed timestamp: str field(default_factorylambda: datetime.now().isoformat()) error: Optional[str] None这个结构看起来简单但每个字段都有用。msg_id用于去重和追踪task_id用于把同一条任务链上的消息关联起来status用于 orchestrator 判断下一步动作error用于失败时记录原因。定义好消息结构之后所有 agent 的输入输出都走这个格式。这样做的好处是你可以在消息总线上加一层日志记录所有交互一目了然。4.3 实现一个最简 orchestratororchestrator 的核心逻辑其实就是一个拓扑排序加执行循环。下面是一个简化版的实现思路def run_dag(dag, registry, initial_input): # dag: {task_name: {agent: agent_name, deps: [task_name, ...]}} # registry: {agent_name: agent_instance} results {} completed set() while len(completed) len(dag): ready [ name for name, node in dag.items() if name not in completed and all(dep in completed for dep in node[deps]) ] if not ready: raise RuntimeError(检测到循环依赖或孤立节点) for task_name in ready: node dag[task_name] agent registry[node[agent]] inputs {dep: results[dep] for dep in node[deps]} message AgentMessage( msg_idf{task_name}-{uuid4().hex[:8]}, senderorchestrator, receivernode[agent], task_idtask_name, payloadinputs or initial_input, ) result agent.handle(message) results[task_name] result.payload completed.add(task_name) return results这段代码没有处理并发、重试和超时但已经能跑通最基本的串行和并行分支。实际使用时你需要在此基础上加上异常捕获、重试计数和超时控制。4.4 写第一个 agent检索 agent 的实现要点检索 agent 的职责很明确接收一个查询返回一组相关结果。实现时要注意几个细节查询改写用户给的原始查询往往不适合直接检索需要先做一次改写把口语化的表达转成关键词组合。结果去重不同来源可能返回重复内容需要根据 URL 或内容指纹去重。结果截断不要把整篇网页塞给下游 agent只保留摘要和关键段落控制上下文长度。来源标注每条结果都要带上来源信息方便下游 agent 引用和验证。class RetrievalAgent: def handle(self, message: AgentMessage) - AgentMessage: query message.payload.get(query, ) rewritten self.rewrite_query(query) raw_results self.search(rewritten) deduped self.deduplicate(raw_results) truncated self.truncate(deduped, max_items5, max_chars2000) return AgentMessage( msg_idfresp-{message.msg_id}, senderretrieval_agent, receivermessage.sender, task_idmessage.task_id, payload{results: truncated}, statussuccess, )这个结构可以直接套用到其他 agent 上。关键是把“输入解析、核心处理、输出封装”三步分开这样每个 agent 的逻辑都清晰可测。5. 跑通之后才会遇到的五个坑5.1 规划 agent 拆得太细或太粗规划 agent 的输出质量直接决定整个流程的效率。拆得太细比如把“写一段话”拆成“写第一句、写第二句”会导致 agent 调用次数暴涨成本和延迟都不可接受。拆得太粗比如把“调研写报告做 PPT”当成一个子任务又回到了单 agent 的老问题。我的经验是每个子任务的预期输出控制在 200 到 500 字之间。这个粒度下agent 有足够的上下文做出有意义的决策又不会因为任务太大而失控。如果规划 agent 拆出来的节点明显偏离这个范围可以在 orchestrator 里加一层校验自动合并过细的节点或拆分过粗的节点。5.2 agent 之间的“接口不匹配”这是最常见也最烦人的问题。检索 agent 返回的是{results: [...]}归纳 agent 期望的输入却是{documents: [...]}字段名对不上跑起来就报 KeyError。解决办法有两个一是在消息总线上加一层适配器负责把上游输出转换成下游期望的格式二是制定统一的内部数据契约所有 agent 都遵守同一套字段命名规范。我倾向于第二种虽然前期麻烦一点但长期来看省事得多。5.3 循环依赖与死锁DAG 理论上不应该有环但规划 agent 有时候会生成带环的依赖图。比如 A 需要 B 的输出B 又需要 A 的输出。这种情况在人工设计流程时很少见但让模型自动规划时出现的概率不低。防御措施是在 orchestrator 里加一个环检测在开始执行之前就用拓扑排序验证一遍。如果检测到环要么让规划 agent 重新规划要么人工介入打断环。不要等到执行到一半才发现死锁那时候已经浪费了不少调用。5.4 上下文在传递中“膨胀”每个 agent 的输出如果都原封不动地传给下游上下文会迅速膨胀。三个 agent 串行下来最后一个 agent 收到的输入可能是原始任务的十倍。控制方法是在每个 agent 的输出环节加一个摘要压缩步骤。比如检索 agent 返回五条结果每条 2000 字直接传给下游就是一万字。如果先让检索 agent 自己做一个 200 字的摘要下游只收摘要加原文链接上下文就控制住了。5.5 失败重试导致的重复副作用如果某个 agent 有副作用比如写文件、发请求重试时可能会重复执行。比如“发送邮件 agent”第一次执行超时了orchestrator 重试结果用户收到两封邮件。解决办法是给每个 agent 调用分配一个幂等键agent 在执行前先检查这个键是否已经处理过。对于有副作用的操作这个检查是必须的。对于纯计算型 agent可以跳过。6. 进阶方向从原型到可用系统的差距6.1 并发执行与资源控制原型里的 orchestrator 是串行执行的但实际任务中很多节点可以并行。比如“检索资料”和“准备模板”这两个子任务没有依赖关系完全可以同时跑。加并发不难难的是资源控制。如果同时发起十个模型调用可能会触发速率限制。需要在 orchestrator 里加一个信号量或令牌桶控制并发度。我的建议是把并发度设在 3 到 5 之间既能提速又不会触发限流。6.2 人工介入节点不是所有任务都适合全自动。有些关键决策点比如“这份报告的核心结论是否成立”最好留一个人工确认环节。agency-agents可以在 DAG 里插入一种特殊的“人工 agent”它的行为是暂停执行、发送通知、等待外部输入。这个机制在内容审核、财务审批等场景下特别有用。实现上人工 agent 的handle方法不返回结果而是把消息状态标记为waiting_humanorchestrator 检测到这个状态就暂停该分支直到收到外部信号再继续。6.3 执行轨迹的回放与调试当系统跑了几十次之后你一定会遇到“这次结果不对但不知道哪一步出了问题”的情况。这时候执行轨迹的回放功能就很重要。实现方式是把每条消息按时间顺序持久化提供一个回放接口可以按 task_id 重放整个执行过程。更进一步可以在回放时支持“修改某一步的输出然后从那里继续执行”这对于调试和优化非常有用。6.4 agent 的版本管理与灰度发布当你开始迭代某个 agent 的提示词或工具集时需要一种机制来对比新旧版本的效果。最简单的做法是给每个 agent 加一个版本号orchestrator 在调用时记录版本信息。这样你可以按版本维度统计成功率和耗时决定是否全量切换。灰度发布则是让一部分任务走新版本、一部分走旧版本观察一段时间后再做决定。这个机制在 agent 数量多、迭代频繁的场景下几乎是必需的。7. 一些个人体会agency-agents这个方向最吸引我的地方不是它能让 AI 多做多少事而是它提供了一种把复杂任务结构化的思维方式。以前我们写自动化脚本习惯性地用 if-else 把所有分支写死现在用 agent 编排可以把“判断”和“执行”分开让每个环节只关注自己最擅长的事。但我也要泼一盆冷水不是所有任务都值得拆成多 agent。如果一个任务用单 agent 加一段清晰的提示词就能稳定完成那就不要为了“架构好看”而强行拆分。拆分的代价是调用次数增加、延迟上升、调试复杂度提高。只有当任务确实复杂到单 agent 搞不定或者对失败隔离有硬性要求时多 agent 编排才划算。另外提示词工程在这个框架里依然重要甚至更重要。因为每个 agent 的职责变窄了提示词的质量对结果的影响反而更直接。一个模糊的“你是一个检索助手”和一个精确的“你是一个学术文献检索助手优先返回近三年内的同行评审论文每条结果包含标题、作者、发表年份和核心结论”效果差距是肉眼可见的。最后分享一个小技巧在开发阶段给每个 agent 加一个“调试模式”开启后会把完整的输入输出打印到控制台。这个功能看起来不起眼但当你面对一个五节点的 DAG 时没有它几乎没法定位问题。等系统稳定了再关掉或者改成只记录到日志文件。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑