你有没有遇到过这种情况:手里有个自动化任务,最开始只需要一段脚本、一个API调用就能搞定。后来需求慢慢变多,脚本里开始塞各种条件判断、重试逻辑、状态管理,最终变成一个谁都看不懂的“面条代码”。我也走过这条路,直到把任务拆给多个各司其职的智能体之后,整个流程才彻底清晰起来。这个项目的名字叫agency-agents,一个专门用来管理和编排多个 AI Agent 协同工作的轻量框架。适合正在做 Agent 应用、多智能体系统、复杂自动化流程的开发者参考,也适合那些觉得“单 Agent 越长越蠢”的人。
1. 为什么我会写一个 Agent 编排框架:单 Agent 的无力感
1.1 从一次自动化尝试说起
几个月前,我在做一个数据采集与报告生成类任务。任务的流程大致是:从多个来源抓取信息,做去重清洗,再按照固定模板生成一份分析报告,最后自动发送到内部渠道。
起初我用单 Agent 实现,把完整步骤全部写进一条超长指令里。看起来没问题,实际操作几次就露馅了——模型经常在中间某一步“忘记”了最前面的要求,或者抓完数据之后开始自由发挥,模板格式变得乱七八糟。我试过加 few-shot、加大模型参数、把指令重写成结构化 JSON,效果都不稳定。真正的问题在于,流程里的每个环节其实需要完全不同的能力:信息检索、文本清洗、摘要生成、格式渲染,这些塞进同一个上下文窗口里,互相干扰,模型越到后面越迷茫。
后来我尝试把任务拆成几个独立步骤,每步由不同模型处理。单个环节质量明显上去,但新的麻烦来了:谁来协调这些步骤?谁来传递中间结果?一旦中间一步失败,是重试还是回退?这些问题用脚本硬写,很快就写成了一坨分不清时序和依赖关系的代码。
这就是我做agency-agents的初衷:与其让一个 Agent 干所有事,不如让一批专职 Agent 像公司里的不同岗位一样分工协作,再由一个调度层来统一安排。
1.2 "agency-agents" 这个名字想要表达什么
英文里 "agent" 既指“智能体”,又有“代理”的意思;"agency" 则是“代理机构、事务所”。一个 Agent 再强,视野也是局部的,把它放进一个组织里,才能发挥整体价值。我取这个名字,就是想表达这套框架的核心思路:管理一个智能体团队,而不是堆砌单个智能体。
它解决的具体问题有三层:
- 角色边界:每个 Agent 只负责一类能力,指令清晰,上下文不被无关信息稀释。
- 任务调度:系统自动把复杂任务拆成子任务,分发给合适的 Agent,并处理依赖关系与失败重试。
- 过程可见:所有 Agent 的输入输出、工具调用、通信记录都可以追踪,方便排查和复盘。
适合用这套框架的场景,几乎都是“多步骤、多决策、强协作”的活儿:研究报告生成、竞品信息整理、内容批量生产、客户工单分类归档等等。它本质上不挑行业,只要任务可以拆成清晰的子任务,就有用武之地。
2. 最基础的一层:Agent 角色定义与系统提示词管理
2.1 Agent 不能只是一个"调用大模型的类"
在我见过的一些项目里,Agent 被简单实现为一层包装:构造函数传入 API Key,内部维护一段对话历史,调用模型就完事。这样做在只有一个 Agent 时没问题,一旦有多个 Agent,立刻会遇到几个尴尬:
第一,所有 Agent 共用同一套提示词模板,职责不分;第二,没有独立的“身份声明”,调度器根本不知道该把任务发给谁;第三,上下文历史保存在对象内部,一重启就丢,无法审计。
所以我在设计agency-agents时,把 Agent 定义成一种可声明的配置结构,而不仅仅是一个类。每个 Agent 必须包括四个核心字段:
| 字段 | 作用 | 说明 |
|---|---|---|
name | 唯一标识 | 调度器通过它路由任务 |
description | 职能描述 | 告诉调度器“这个 Agent 擅长什么”,是路由判断依据 |
system_prompt | 系统指令 | 真正指导模型行为的角色设定 |
allowed_tools | 工具白名单 | 限定它能调用哪些工具,做权限控制的最小单元 |
举个例子,我定义了一个“数据研究员”角色:
agent: name: researcher description: 负责搜索、收集、整理外部资料,输出结构化的信息清单。不负责生成最终报告。 system_prompt: | 你是一个专业的研究员。你只负责收集信息,并按照用户给出的字段结构输出事实性描述。 不要生成任何观点或建议,不要修改用户提供的字段结构。 如果信息不足,请明确输出“信息不足”,不要编造。 allowed_tools: [web_search, http_get, doc_parse]description写得好不好,直接决定调度器的路由准确度。很多人在这个字段上写得很空,例如“负责各种任务”,调度器一看,所有 Agent 都能接,等于没写。我的经验是:描述时要写清楚“擅长什么”和“不负责什么”,边界越清晰,调度越准确。
2.2 角色定义模板,我踩过的第一个坑
角色模板听起来简单,实际写过一轮才会发现里面全是细节。
第一版我图省事,把所有 Agent 的 system_prompt 都写在一段很长的话里,开头是“你是全能助手”,后面补充“但现在你需要扮演研究员”“现在你需要扮演写手”之类的话。一轮跑下来,最典型的症状是角色漂移:写手 Agent 在最后一段开始自己“查资料”,研究员 Agent 在输出里加入了“我觉得”之类的观点。
解决方式是让 system_prompt 做的事情尽量少且绝对。每个 Agent 的提示词都遵循三原则:
- 身份唯一:开场就声明“你是XX角色”,整段提示词不再出现其他角色。
- 边界明确:明确写出“你不做XX”,比如“你不负责输出建议”“你不做格式美化”。
- 输出严格:给模型指定固定输出结构,并强制 JSON 输出,避免自由发挥。
还有一个很容易被忽略的小坑:系统提示词不宜过长。太长不仅烧 token,而且模型会“淹没”在指令中,对关键约束的执行力反而下降。我的经验是,核心系统提示词控制在 800 字以内,超出部分放进详细的工具说明或外部参考文档里,需要时再通过检索取用。
3. 任务编排引擎:从链式调用走向 DAG
3.1 三种常见的编排方式与取舍
多 Agent 之间的任务流如何组织,是整个框架最核心的设计点。我调研了几种常见编排方式,各自适用场景完全不同:
- 链式编排:A 完成后把结果传给 B,B 再传给 C。最简单,适合固定流水线。缺点是中间任何一环失败,后面全部卡住;而且依赖关系是隐式的,代码里看不出来。
- 路由编排:由一个调度 Agent 判断任务类型,动态选择后续的处理 Agent。适合分类、分流场景,但调度 Agent 自身的判断准确性会成为瓶颈。
- DAG 编排:把任务流定义成一张有向无环图,节点是 Agent 动作,边是数据依赖。支持并行执行、失败重试、条件分支,表达力最强,也是我最终的选择。
我最终选择了 DAG,不是因为“听起来高级”,而是因为实际任务的依赖关系天然是网状的:数据清洗要等数据收集完成;报告生成要等清洗结果,但摘要生成可以和报告生成并行。这种关系用链式根本无法描述。
3.2 用 DAG 描述任务流,一个实际的配置示例
在agency-agents里,我用 YAML 描述任务流。下面是一个简化示例,真实项目里可以照这个结构去扩展:
pipeline: name: daily_report steps: - id: collect agent: researcher input: "{trigger.input}" - id: clean agent: cleaner input: "{collect.output}" - id: summary agent: summarizer input: "{clean.output}" depends_on: [clean] - id: render agent: writer input: "{summary.output}" depends_on: [clean, summary]关键设计有两点:
一是节点间的数据传递通过引用表达式完成。比如{clean.output}表示取clean节点的输出作为当前节点输入。这样依赖关系一目了然,不再是隐藏在代码里的变量传递。
二是执行引擎支持广播并行。没有依赖关系的节点(比如某些独立的数据抓取任务)会并发执行,整体耗时大幅缩短。对于每天上百次的自动执行任务,总时间能从 15 分钟压到 4 分钟左右,提升非常明显。
还有一个细节:调度器给每个节点加了超时限制与自动重试。Agent 调用大模型偶尔会卡顿或返回空结果,针对掉线失败,重试一次;因为输入数据不合格导致的失败,则不重试,直接抛出错误。两者区分很重要,盲目重试反而会掩盖数据质量问题。
4. 工具注册中心与权限治理:不让 Agent 乱调东西
4.1 Tool Registry 的核心契约
Agent 的能力不能只靠“说”,还得通过工具去“做”。但多 Agent 环境下,如果每个 Agent 都能随手调用任何工具,风险会非常大:一个角色写错了提示词,就可能导致 Agent 去调用不该调用的写接口、删除接口。所以工具必须收敛到一个统一注册中心管理。
在agency-agents中,每个工具注册时都要提交一份结构化声明,大概长这样:
tool: name: http_post description: 发送 HTTP POST 请求,用于提交数据到指定接口 parameters: url: type: string required: true body: type: object required: false permission_level: L2这里permission_level是我加的一个分级字段:
| 级别 | 含义 | 示例 |
|---|---|---|
| L0 | 只读,无副作用 | 读取本地配置、查询文档 |
| L1 | 内部计算 | 文本清洗、格式转换 |
| L2 | 外部 API 调用 | 调用第三方搜索、发送 HTTP 请求 |
| L3 | 写操作,有副作用 | 写数据库、发送消息、执行删除 |
每个 Agent 定义中的allowed_tools,本质上就是它的权限清单。调度器在执行前会做一次静态校验,阻止 Agent 调用清单外工具;执行时还会拦截权限级别过高的工具。这样做的好处是,即使某个 Agent 的输出被提示词注入干扰,它也碰不到不该碰的工具。
另一个很实际的好处是工具参数合法性的前置校验。大模型经常编造参数,比如 URL 少个前缀、JSON 字段拼错。工具注册中心会在真正执行前用 JSON Schema 做一次参数校验,不合格直接拒绝,而不是把脏数据发给外部接口。这一步帮我挡掉了很多低级的线上事故。
4.2 沙箱隔离,不只是安全防护
安全性不光是“别删库”,还有“别互相拖累”。多个 Agent 并发执行时,如果一个 Agent 的工具调用占用了全部内存,其他 Agent 就会一起遭殃。我在沙箱层做了三类限制:
- 资源限额:每个 Agent 的工具调用有独立的内存、线程数配额,超了就杀死本轮任务,不影响其他 Agent。
- 网络策略:默认禁止 Agent 直接访问内网地址,除非在配置里显式放行。避免因提示词注入导致的 SSRF 风险。
- 存储隔离:Agent 的中间产物只能写入任务专属目录,不能跨目录读写别人文件。
关于这套设计,我想强调一个理念:权限治理不是上线前才考虑的,而是 Agent 框架的骨架。没有权限约束的多 Agent 系统,越跑越危险。很多问题平时不发生,但一发生就是生产事故级别的。
5. 消息总线与上下文管理:多 Agent 通信的关键
5.1 为什么要独立一个总线而不是直接函数调用
最开始我做多 Agent 协作时,用的是最省事的方式:Agent A 处理完,直接把结果作为参数调用 Agent B 的方法。写起来很快,但用一段时间就发现问题了——拿到结果之后,根本不知道这条数据经历了哪些环节;出了错也找不到责任节点;想并发执行更是无从下手。
于是我在框架里加了一层消息总线。Agent 之间不直接互相调用,而是把消息发给总线,由总线和调度器决定消息该送到哪里。每条消息都带这些字段:
| 字段 | 作用 |
|---|---|
topic | 消息主题,决定消息进入哪个队列 |
correlation_id | 关联 ID,贯穿一次完整任务 |
sender/receiver | 发送方与接收方,没有则为广播 |
payload | 具体数据,通常是 JSON |
deadline | 消息超时时间 |
这个设计带来的好处,说起来其实是三个:解耦、审计、可恢复。接口调用关系一旦写在代码里,重构就会非常痛苦;消息总线把所有调用变成可配置的流动,换掉一个 Agent 不会影响上下游。每条消息都有完整的链路记录,出任何问题可以按correlation_id一步步查回去。再加上所有消息都会被持久化,系统崩溃后可以从最近一条成功消息继续执行,不用全部重跑。
5.2 上下文隔离的规则
上下文管理,是多 Agent 系统里最容易踩坑的地方,没有之一。
一个 Agent 处理任务时,理论上只需要与“本任务”相关的上下文。如果调度器把全量历史一股脑塞进去,模型很快就会被无关信息干扰。比如研究员的中间结果里有一大段原始资料,写手只需要摘要部分,结果全传过去了,写手就可能照着原文“改写”而非“根据摘要创作”,输出风格完全跑偏。
我在框架里定了一条硬性规则:Agent 之间传递的上下文,必须是经过提炼的中间产物,而不是原始数据堆。
我给每个 Agent 的输出结构做了约定,统一为三段:summary(摘要结果,用于传递给下游)、detail(细节数据,仅在特定需求时传递)、meta(元信息,包含处理时间、数据来源等)。下游 Agent 默认只看summary,需要深挖才展开detail。这很像现实中公司里部门之间只给“结论摘要”而不是“全部票据”的做法。
上下文的隔离,还体现在窗口溢出问题的预防上:每个 Agent 的对话历史只保留最近 N 轮,而不是无限累积。超长任务会被切成多轮完成,每轮之间通过消息汇总状态,保证不突破模型上下文限制。
6. 可观测性设计:调试 Agent 系统的基本功
6.1 日志、追踪、状态快照,一个都不能少
Agent 系统的失败方式和传统软件完全不同。传统软件要么报错,要么给出错误返回值;Agent 系统却经常是“看起来成功了,实际上结果完全不对”。比如写手成功输出了一篇漂亮的报告,但数据来源全是编造的;研究员成功返回了信息,但关键观点漏掉了。“成功”和“正确”之间的鸿沟,只有靠可观测性来弥合。
agency-agents的可观测性分三层落地:
- 第一层:结构化日志。每个 Agent 接收什么输入、调用了哪些工具、返回什么输出、耗时多久,全部记录成 JSON 日志。
- 第二层:追踪链路。通过
trace_id把所有相关步骤串起来,可以在界面上按一次任务查看全流程,一眼定位到是哪个环节出了问题。 - 第三层:状态快照。每隔一定时间,把当前所有 Agent 的上下文、消息队列、任务进度打成快照,支持崩溃恢复和现场分析。
6.2 从一次"死循环事故"看可观测性的必要性
调试阶段遇到过一件特别典型的事。我当时定义了一个“信息补全” Agent,它的任务是检查输入信息是否完整,不完整则发起“补充信息”请求。结果因为指令描述有歧义,这个 Agent 发现自己缺少信息后,不是去找外部工具,而是给自己发消息:“我需要更多信息。”它收到这条消息后,发现自己还是缺信息,于是又给自己发了一条……如此循环下去,根本停不下来。
如果不是做了日志追踪,我会以为任务很慢是因为模型在“认真思考”,实际上它已经进入了一个死循环。看日志后才恍然大悟:这 Agent 3 分钟里给自己发了 200 多条一模一样的消息,白白烧了大量 token。
针对这类问题,我在框架里加了几层保护:
- 自我调用限制:单个 Agent 在一条任务链里最多自我调用 3 次,超出即熔断。
- 循环检测:如果消息总线里连续出现 5 条以上完全重复的主题消息,自动终止对应任务。
- 成本熔断:每一次任务的 token 消耗超过预设预算,直接中止,并把中间结果保存下来。
这些限制在常规开发时看似多余,但放到生产环境,每一层保护都是在替你兜底。做 Agent 应用,可观测性不是“以后再说”的事,而是从写第一行代码时就应该铺好的地基。
7. 我在实际搭建中总结的避坑清单
7.1 模型输出不稳定,如何让结果可控
模型输出不稳定是 Agent 应用最头疼的问题。再严密的提示词,也无法 100% 保证输出格式统一。我的做法是“双重保险”:第一步,让模型按 JSON Schema 输出结构化结果;第二步,把输出拿到代码里做二次校验。格式不合法就自动重试一次,重试后仍不合规则走失败处理。
这里有个小技巧:重试时不要简单地重复提示,而是把前一次输出的错误原因(比如“缺少必填字段 xxx”)一并告诉模型,它能更容易地修正自己。实测下来,重试一次的成功率能提高很多,整体失败率可以接受。
7.2 成本失控的防范
多 Agent 系统比单 Agent 的 token 消耗大得多。每个 Agent 都有独立的系统提示词、工具定义和中间输出,光是消息传递就会放大开销。我的经验是别等月底看账单,而是在框架层面做预算控制:
- 每个 Agent 有单次任务的 token 配额,超了自动熔断。
- 每个任务有总预算,包含所有 Agent 的开销,超了立即终止。
- 对中间结果做有损压缩,能传摘要绝不传全文。
还有一条很反直觉的经验:不要为了提高准确性而无限增大模型规模。在 Agent 系统里,很多失败不是模型能力不够,而是指令和上下文出了问题。先把编排、校验、追踪做好,再考虑换更大的模型,往往性价比更高。
7.3 多 Agent 协作的收敛与终止条件
Agent 系统最怕的就是任务“发散”——Agent 们越是自由,越容易跑偏方向,最后交出一个完全不是用户想要的产出。我后来养成了一个习惯:在设计任务链时,就给整条链定义一个明确的“完成条件”。
比如生成报告的任务,完成条件就是“摘要节点已输出、报告节点已渲染、权限校验已通过”。一旦满足条件,立即终止后续节点,而不是等所有 Agent“自然跑完”。
为了这条规则,我在调度器里加了一个最大步数限制。无论任务多复杂,一条流水线最多执行 N 步(N 可以根据复杂度调),超过就强制停止,返回当前最优中间结果。这个设计看起来“粗暴”,却有效防止了 Agent 在不必要的环节上反复空转。
8. 写在最后:这套框架的适用边界与我的经验
做了这么久的 agent 编排,我越来越确信一件事:多 Agent 系统的复杂度,主要不在于“让模型变得更聪明”,而在于如何用工程手段约束一批有自主性的组件,让它们按预期协作。
agency-agents擅长处理的是“可拆解、有边界、有稳定输出要求”的任务。如果任务本身就是完全开放式的探索,或是需要高度创造性发挥的场景,那这套约束可能反而会限制住模型的空间。选不选多 Agent,本质上取决于任务能不能拆、拆开之后的边界稳不稳定。
还有一个小建议给正在上手的人:第一次跑通流程后,先别急着加功能,而是把所有 Agent 调成 verbose 模式,跑一轮真实任务,把每一层的输入输出都打开看一遍。很多人花了大量时间调提示词,却很少去看模型“实际收到了什么、实际输出了什么”。而这往往才是问题所在。
这套框架还有很多可以扩展的方向,比如引入人工审批节点、针对具体领域做专用 Agent、支持更复杂的动态规划策略。但作为第一版,我认为“先把约束建好”比“先把能力铺满”更重要。先把流程变得可控,再逐步放开自由度,这条路我走下来是稳的。