1. 为什么会有Agent-Reach:多Agent协作失控的现实问题
先说结论:Agent-Reach不是什么复杂的平台,它是一层让多个AI Agent能互相找到、传递任务、交接上下文的协调层。我做它的直接原因是,单Agent的能力已经撑不住真实业务了,但多Agent一起上之后,失控得比想象中快得多。
1.1 单Agent的极限和多Agent的混乱
如果你自己搭过Agent应用,应该会有这种感觉:单个Agent做得再花,它能调的工具、能记住的上下文、能连续处理的步骤都是有上限的。我最早做的一个调研助手,塞了十几种工具进去,提示词写了快三千行,结果指令一复杂,它就开始自己跟自己打架——刚查完的数据,下一步又去重查一遍;用户问A它回答B,追问一句它又把之前的结论推翻了。不是模型不聪明,是人类把一个Agent塞了太多职责,它根本忙不过来。
所以很自然的想法就是拆分:搞一个调研Agent、一个写作Agent、一个查数据库的Agent、一个发邮件的Agent,各管一摊。拆分之后确实清爽,但紧接着问题就来了——这些Agent各跑各的,谁也不认识谁。调研Agent得出结果之后,怎么把结论交给写作Agent?写作Agent需要查数据的时候,它怎么知道"查数据的活"该发给谁?更别说中间如果某个Agent挂了,其他Agent还在傻等它的回包。
我翻了市面上一些编排框架,要么太重——带UI、带画布、带一大堆我不需要的模块;要么太轻——只解决了"怎么调用",没解决"怎么发现"和"怎么交接"。这个时候我就决定自己写一个,名字就叫Agent-Reach,词根是"reachability",我理解的是两个意思:一是Agent之间的可达性,二是任务最终能不能"够得着"正确的承接方。核心就三件事:注册、路由、交接。
1.2 一个真实的失败案例
提一个让我下决心写这套东西的具体事故。当时我跑了三个Agent做一个"行业周报"的活:数据采集Agent负责去抓信息,分析Agent负责整理观点,排版Agent负责生成Markdown文档。一开始用最简单的HTTP调用串起来,A跑完调B,B跑完调C,看起来没毛病。
第一次跑就翻车了。采集Agent返回的是一份JSON数组,分析Agent解不出来,直接报错退出,后面的排版Agent根本没被调用。我当时的第一反应是加错误重试,但重试之后发现另一个问题:分析Agent失败的时候,采集来的原始数据放在临时文件里被后面的任务覆盖了,重试等于从头再采一遍。再后来,三个Agent同时跑的时候,谁先谁后靠的是固定顺序,一遇到"分析Agent说这里缺数据,得回头让采集Agent补采"这种回环场景,整个链路就卡死了。
这些问题的共同根源是什么?Agent之间没有"相互了解"的机制,也没有一个地方记录"谁现在有什么、谁还活着、谁上次完成任务用了多久"。这些信息在分布式系统里叫服务发现和健康检查,到了Agent系统里居然没人做。我当时就想,如果Agent之间能像微服务一样做注册和发现,很多问题其实有现成的解法。Agent-Reach的雏形,就是把微服务那套思路搬到Agent协作上。
1.3 设计目标:把"找人干活"变成协议问题
在动手之前,我给自己定了三条设计原则,后面所有功能都是围绕这三条展开的:
第一,Agent不需要知道"对方是谁",只需要知道"谁能处理这种任务"。也就是讲,调用方发出一条带意图的任务,由调度层去匹配注册表,找到合适的承接Agent。这样一来,新增Agent、替换Agent、下掉Agent,对调用方完全透明。
第二,交接必须带上下文,不能只传一句"请继续"。上下文在这里不是指把所有聊天记录都塞过去,而是指经过结构化的任务目标、已收集到的关键信息、已执行的动作历史、当前状态标记。宁可传得瘦一点,也不能丢关键字段。
第三,单点不能成为瓶颈。注册中心挂了,Agent之间至少要能按缓存的路由信息继续工作,不能因为一个中心服务挂了,整个协作网络瘫痪。
这三点定下来之后,Agent-Reach的架构就比较清晰了:一个轻量注册中心,负责收集各Agent的能力声明和健康状态;一条路由引擎,负责把任务意图匹配到具体的Agent;一套Agent SDK,在业务Agent里做接入封装。接下来的篇幅,我按这个顺序把每个部分怎么实现的拆开讲。
2. Agent-Reach的核心机制:注册、发现与路由三层设计
不管外面把Agent编排吹得多玄乎,落到代码层面,逃不开三个动作:报名(注册)、点名(发现)、派活(路由)。只不过Agent场景里,这三个动作各有各的特殊性。
2.1 能力注册表:Agent怎么自我介绍
Agent注册不是启动的时候报个到就完了,要把"我会干什么"变成机器能读懂的结构化信息。我用的是一份能力声明(Capability Manifest),本质上是JSON,里面包含三个部分:agent_id、技能描述列表、运行时信息。
我先给一个最小示例:
{ "agent_id": "research-agent-v1", "display_name": "调研分析Agent", "skills": [ { "skill_id": "web_research", "description": "根据主题抓取网络公开信息并汇总为结构化报告", "input_schema": { "type": "object", "properties": { "topic": { "type": "string" }, "depth": { "type": "integer", "minimum": 1, "maximum": 3 } }, "required": ["topic"] }, "output_schema": { "type": "object", "properties": { "summary": { "type": "string" }, "sources": { "type": "array", "items": { "type": "string" } } }, "required": ["summary", "sources"] } } ], "runtime": { "endpoint": "http://10.0.1.5:8100/execute", "transport": "http", "heartbeat_interval_sec": 5 } }这里有个容易忽略的细节:注册表里必须同时带input_schema和output_schema。只有输入没有输出,路由的时候就只能知道"这个Agent能接活",但不知道它干完活会交回来什么格式,下游Agent拿到结果还得猜。我把输出Schema也强制要求,这样A的输出可以直接按Schema映射到B的输入,交接就顺了。
注册的时机也值得说。我推荐两种结合:Agent启动的时候主动注册一次,之后每隔几秒发一次心跳,心跳里带上最近的负载情况(正在跑的任务数、平均耗时)。主动注册解决"上线可见",心跳解决"状态新鲜度"。大批量Agent滚动更新的时候,旧实例的注册信息要有TTL过期机制,默认给90秒,没心跳就自动摘掉,防止路由到僵尸节点。
2.2 意图路由:任务怎么找到对的Agent
路由是Agent-Reach看起来最"智能"、实际最需要务实对待的部分。我对路由的第一步不是语义匹配,而是标签匹配。每个Agent会声明一组tag,比如"research"、"writing"、"database"、"notification",然后注册中心维护的是一个tag到agent列表的倒排索引。任务进入的时候先带tag,直接定位候选集合,这一步是纯查表,毫秒级,稳。
第二步才轮到语义匹配。不啰嗦,先看代码:
from agent_reach import Router, Task async def route_task(task: Task, router: Router): # 先按tag缩小范围 candidates = await router.filter_by_tags(task.tags) if not candidates: return None # 对候选做输入字段校验,不匹配的直接淘汰 valid = [] for agent in candidates: agent_schema = agent.skill.get("input_schema", {}) if validate_json_schema(agent_schema, task.payload): valid.append(agent) if not valid: return None # 最后按健康度和历史成功率排序 ranked = sorted( valid, key=lambda a: (a.health_score, a.history_success_rate), reverse=True ) return ranked[0].agent_id我把语义匹配放在最后一步,而且只在tag结果为空或者多个候选得分都差不多的时候才启用大模型来解析。为什么这么设计?因为在大流量场景下,每一步语义匹配都是成本和延迟,纯靠大模型做路由,QPS一上来就撑不住。先用确定性规则把98%的任务分流掉,剩下2%的模糊请求才用模型兜底,这个比例我在实测中调过,既保住了速度,也保住了长尾准确率。
还有一种常见情况:没有任何Agent能接这个任务。这时候不要硬派,要返回一个"未路由"信号,让发任务的Agent自己决定是降级还是终止。我见过不少编排框架在这里直接抛异常,用户端的体验就是"Agent突然不说话了",这种哑失败最讨厌。
2.3 心跳与健康度:怎么判断一个Agent还活着
心跳这事听着简单,就是定时上报嘛,但实际坑不少。第一个坑是心跳风暴:200个Agent同时每3秒发一次心跳,注册中心每秒要处理大几百个请求,虽然不算多,但如果你还在心跳里带负载数据,每次都是一个不小的JSON,积少成多也够喝一壶。
我的做法是分层上报。频率高的心跳只带三样东西:agent_id、timestamp、一个轻量的load_level数值;频率低的离线或慢速上报才带详细负载和最近任务统计。注册中心只根据高频心跳更新"最后存活时间",根据低频上报更新路由权重。这样两者不互相干扰。
健康度评分我用了四项指标加权,参数如下:
| 指标 | 权重 | 说明 |
|---|---|---|
| 心跳新鲜度 | 30% | 距上次心跳超过3个周期,扣分;超过TTL,直接下线 |
| 任务成功率 | 40% | 最近50个任务成功比例,长窗口避免单次抖动 |
| 平均耗时 | 20% | 相比该Agent自身历史均值,超过2倍则扣分 |
| 并发负载 | 10% | 当前并发超过声明上限,降权 |
这个评分不是摆设,它直接影响路由排序。你可以理解成:每个Agent头上顶着一个实时变化的分数,路由请求来了,分数高的优先接活。我见过有人问,为什么不用随机负载均衡?因为Agent不是无状态的服务,它是有状态、有上下文、有记忆的,同一个Agent连续处理相关任务,成功率远高于频繁切换Agent。健康度加权的本质就是尽量把任务留在"状态最热"的Agent手里。
3. 关键设计决策与取舍:哪几个决定后来证明对得起功夫
这一段我想讲三个具体的选择,都是做的时候犹豫过、事后觉得幸好没走弯路的地方。
3.1 为什么用JSON Schema描述能力而不是自然语言
最早我想过用自然语言写Agent的能力描述,比如"本Agent擅长搜索网络并总结信息",然后把任务描述扔给大模型去匹配。好处是灵活,坏处是——完全不可控。同一个任务描述换几个词,匹配结果就变了;两个Agent能力写得很像的时候,模型经常随机挑一个,而不管谁的状态更好。
JSON Schema的好处是约束强、可校验、可排序。input_schema里required字段直接决定了这个任务能不能被这个Agent处理,不需要模型猜。路由的时候我可以精确比对:任务里的字段是否齐全、类型是否匹配、范围是否合法。这些是纯代码逻辑,不需要走大模型,稳定性和速度都更有保障。
当然,纯Schema也有缺点,就是表达不了模糊的意图。所以我最后做的是双轨:标签+Schema是第一轨,语义匹配是第二轨。第一轨保下限,第二轨拉上限。这个组合在真实跑下来的效果是,第一轨能接住约80%的任务,剩下20%才需要语义兜底。
3.2 同步调用还是异步消息:被现实逼出来的选择
Agent之间的任务交接,到底用同步HTTP等结果,还是用消息队列异步收?我一开始偏向后者的,觉得异步才高级、才解耦。但实际在业务里跑起来,发现一个尴尬的局面:大部分调用方(比如一个聊天机器人、一个定时任务)就是要一个最终结果,它不关心中间有多少个Agent接力。如果中间全部拆成异步消息,调用方得自己去轮询或者订阅"最终完成"事件,这反而把复杂度推给了用户。
最后Agent-Reach采用的是"外部同步、内部异步"的折中方案。外部接口保持同步:调用方发一个任务,等一个最终结果,超时上限是业务配置的,默认300秒。内部流转用异步消息:路由之后,Agent之间的交接通过内部消息总线走,每个节点完成就把结果发给下一个节点,最后一个节点完成后由总线把结果回给调用方。
这个折中心得可以写进教科书——接口设计要考虑调用方的体感,别让内部的最优解变成外部的负担。我见过有些框架强行全异步,对接方得额外写一堆回调代码,本来两分钟能解决的事变成两小时,这是过度设计了。
3.3 上下文交接格式:不丢上下文才能算reaching
"上下文交接"这四个字,一开始我天真地以为就是把上一轮对话都拼到下一轮的提示词里。后来发现完全不是。对话记录里面有大量无关紧要的赘述、用户临时改口的旧指令、甚至Agent自己犯过的错,这些全部塞给下一个Agent,只会稀释它对核心任务的理解。
Agent-Reach里定义的交接上下文,是一个固定结构,核心只有五类字段:
{ "task_id": "t_20250318_001", "goal": "生成一份关于XX行业的调研报告", "collected": { "key_facts": ["..."], "source_links": ["..."], "user_preferences": ["..."], "already_done": ["已完成数据采集,共12条", "已完成初步分类,3大类"], "blockers": ["部分数据源需要重新采集"] }, "current_state": "data_collected", "next_expected_steps": ["写作Agent基于collected生成报告初稿"], "constraints": { "max_report_length": "1500字以内", "style": "技术文档风格", "deadline": "2025-03-20 18:00" } }我特别想强调两个字段:already_done 和 next_expected_steps。前者是防止下一个Agent重复劳动——它一看到"数据采集已完成"就不会再去采一遍;后者是给下一个Agent一个明确的起点——“你该从这里开始干活”。这两个字段让我多次避免了下游Agent“重蹈覆辙”或者“理解偏了继续跑偏”的情况。
这个上下文的格式我有意识地做得比聊天记录轻很多,实测下来,交接的token开销只有完整聊天记录的五六分之一,但有效信息的密度反而更高了。核心原因是筛选过、压缩过的关键信息,比海量原文更能引导Agent集中在正事上。
4. 落地部署:从实验到能稳定跑的踩坑记录
Agent-Reach本身并不复杂,一套Python写的注册中心加SDK,但真正把它从demo状态推到能稳定跑业务,踩了不少我没想到的坑。分环境搭建、踩坑记录、性能实测三个阶段说。
4.1 环境准备与最小可用部署
运行Agent-Reach需要一个Python 3.10以上的环境,注册中心是aiohttp写的异步服务,SDK是同步兼容的客户端,对业务代码侵入性很低。最小部署只要三步:
- 启动注册中心服务:
python -m agent_reach.server --config server.yaml - 业务Agent引入SDK,注册能力:
from agent_reach import AgentRuntime; runtime = AgentRuntime().start() - 调用方通过SDK发任务:
client.submit_task(task, callback=handle_result)
我的建议是注册中心和业务Agent部署在同一套网络环境里,不跨机房。因为心跳和路由对延迟敏感,跨机房哪怕只多10ms,心跳超时设置就得跟着放松,一放松,故障检测就迟钝了。如果你确实有多地部署需求,每个地域起一个注册中心实例,然后再做地域间的注册信息同步,而不是直接中心化。
配置文件里我值得提的一个参数是route_timeout_grace,也就是路由超时的宽限期。默认我设3秒。有些Agent处理任务前要做内部初始化,比如加载一个本地模型,可能要花几秒才真正开始干活。如果不给它这个宽限期,注册中心会误判它超时,把它健康分拉低,导致后面路由总绕开它。这一点在接入本地模型Agent的时候尤其重要。
4.2 踩坑记录:注册表死锁、心跳风暴、超时设置
先说注册表死锁。我第一版用的是键值对的注册表,内部用一个dict存agent信息,路由的时候遍历匹配。某次压力测试,几百个Agent同时注册,加上路由请求并发进来,dict的遍历时间到了毫秒级,看起来还能扛。结果一个Agent下线清注册信息的时候,没用锁,另外一个请求正好在读同一份数据,整个注册进程直接卡死。修法很简单:注册表改成带读写锁的结构,写操作加写锁,路由只用快照副本。读的时候永远不碰在用写的数据,这是并发编程里最基本的一课,我在Agent-Reach里又复习了一遍。
心跳风暴是另一个有意思的坑。心跳间隔设太短,Agent数量一多,注册中心CPU就飙升;设太长,又发现不了故障。最后我用的解决方案其实在上面提过,就是分层上报。这里再补一个细节:心跳的抖动(jitter)很重要。如果所有Agent都用固定5秒心跳,它们的请求会高峰性地撞在一起,CPU使用率呈现周期性尖刺。我在SDK里对心跳间隔加了±30%的随机抖动,尖刺立刻平缓了很多。这个技巧在物联网领域叫jitter randomization,在Agent协作里同样适用。
超时设置也踩过坑。一开始我给的取值是任务最长运行时间,比如写报告要10分钟,超时就设10分钟。结果发现一个问题:如果这个任务在第一个Agent就卡死了,后面所有环节都在等着它,任何重试机制都没有意义。所以我把超时拆成"节点超时"和"总超时"两层:节点超时按每个Agent声明的平均耗时乘2倍,总超时是整条链路的兜底。哪个先到都触发处理,节点超时会触发同级重试或降级,总超时只触发终止并返回部分结果。这个设计看起来不起眼,但真正把"一个任务卡死整条链"的问题解决了。
4.3 性能实测与调优方向
我把Agent-Reach跑在一个四核8G的普通云服务器上,注册中心在这台机器上,业务Agent分布在另外两台机器。压测场景是100个并发任务,每个任务平均经过3个Agent节点,每个节点平均耗时要模型推理时间约20秒。这时候注册中心本身的压力其实不大,真正的瓶颈在各Agent的处理并发上。
注册中心的单机吞吐,纯路由请求能达到约每秒1500个,我认为对中小规模Agent集群(几百个以内)完全够用。如果要继续压,瓶颈通常在注册表的锁竞争和心跳数据的内存占用。我建议的方向是:注册表拆分——把"在线状态表"和"能力路由表"分开,两者用不同的数据结构维护。在线状态表用简单的哈希表+过期队列,能力路由表用倒排索引。这样即使在线状态刷得很频繁,路由表的锁竞争也几乎不受干扰。
5. Agent-Reach在真实业务中的三种用法
说了这么多原理,来点实际的。我把Agent-Reach用在了几个不同形态的业务里,效果和调优方向都不一样,选三个有代表性的讲讲。
5.1 应用一:多Agent协作写技术方案
这是我最开始的场景。三个Agent分别是:资料采集Agent、方案架构Agent、文稿润色Agent。用户给一个需求,比如"设计一个订单超时自动关单系统",流程是:
资料采集Agent先抓取业界常见方案(消息延迟、定时扫描、状态机),把采集结果结构化之后,带着上下文交给方案架构Agent;架构Agent基于这些资料给出技术选型建议;最后润色Agent把技术语言转成项目文档。这个场景里,上下文交接的价值体现得最明显——如果没有already_done字段,架构Agent经常会自己去重复搜索,浪费时间不说,还容易把采集阶段没有过滤干净的垃圾信息当成依据写进方案。
跑了一段时间的经验是,给采集Agent的depth参数要限制在2以内,超过2它容易陷入深度爬取,产出大量没用的中间笔记,交接给下游的时候,上下文体积暴涨,反而不利于后续Agent的注意力。这类"上下文卫生"问题,是在实际跑业务后才一步步体会到的。
5.2 应用二:客服兜底路由
另一个场景是一个客服系统。用户的问题先进一个简单分类器,分类器拿不准的就抛给Agent-Reach,让不同领域Agent来认领。这里路由的重点是:必须支持"拒单"。
接线Agent有时候会遇到自己处理不了的问题。比如用户问的是退款流程,但这次退款涉及法律纠纷,负责售后的Agent处理不了,它可以在执行前向注册中心返回一个"reject + reason"信号,注册中心收到之后重新路由给法务Agent。这个信号我在最开始设计的时候没有,后来加上去的。在Agent协作里,承认自己不能处理并及时转交,比硬撑着做一个错误回答重要得多。
我在这里也加了一条路由规则:如果同一个任务被两个Agent连续拒单,就停止自动路由,降级给人工处理队列,而不是无限循环踢皮球。这条规则拯救了我很多次,踢皮球是Agent协作里最容易被忽视的失败模式。
5.3 应用三:跨系统的定时任务编排
第三种用法是定时任务编排。比如每天凌晨,数据同步Agent处理完日志,通知数据分析Agent生成报表,再让报表Agent推送到群里。整个过程没有用户实时参与,但链路更长,涉及四个Agent。
这个场景里我最看重的是可观测性。Agent-Reach给每个任务生成一个task_id,所有节点处理完都把日志挂在这个task_id下面,注册中心维护一个任务流转记录。排查"今天报表为什么没发"这种问题的时候,我只需要按task_id查一下,看卡在哪个节点,几秒钟就能定位。这个日志关联设计虽然简单,但如果你不在第一天就做进去,后面业务复杂了再去补,成本极高。Agent协作跟微服务一样,链路追踪绝对不是锦上添花,是必要设施。
6. 什么场景不适合Agent-Reach,以及我的后续打算
讲完用法,也把我的反面经验说透。不是所有多Agent场景都适合用Agent-Reach,我自己划了一条清晰的边界线,省得你用错方向。
6.1 边界:哪些场景别硬套Agent-Reach
第一种不适合的场景是单Agent内部的多步任务。如果一个Agent自己调用工具就能完成整个流程,那就别为了让某个环节看起来"独立"而强行拆成多个Agent。拆出来的每个Agent都要维护能力声明、心跳、上下文交接,这些都不是免费的。如果拆开之后它们之间其实没有并行的需求和独立的记忆状态,那拆了纯属给自己找麻烦。
第二种不适合的场景是强依赖人类审批的工作流。Agent-Reach默认的交接是自动的,某些环节如果需要人来确认,比如"这笔退款是否执行",你不能指望Agent自己决定,必须要有一个人工闸点(human-in-the-loop)。我的做法是在交接上下文里加一个approval_required字段,含有这个字段的任务会被路由引擎挂起,等审批回调通过后才继续流转。这套机制我做得不算完美,但至少把强审批场景和全自动场景区分开了。如果你做的就是纯审批流,用专门的工作流引擎更合适。
第三种不适合的场景:需要实时流式响应的对话场景。Agent-Reach的同步接口默认是等全部Agent跑完才返回结果,如果你的业务要求用户输入一句就立刻收到一个字一个字蹦出来的响应,那它不适合。我后来在流式场景里是用回调方式,Agent每出一个中间结果就推一次给调用方,但这需要调用方配合改接口形式,比普通同步调用麻烦。
6.2 后续最有价值的两个扩展方向
按我目前的实际需求,排在前面的是两件事。
一件是路由策略插件化。现在路由是内置的标签匹配+语义兜底,但我已经碰到了几种没有覆盖到的业务诉求:有的任务希望按成本最优路由(选最便宜的模型Agent),有的希望按隐私域路由(某些数据不能出内网)。把路由规则从注册中心里抽出来,做成可插拔的策略接口,会是下一步比较值得做的改动。
另一件是任务级追踪链路的可视化。现在task_id的日志已经能支撑手工排查,但当Agent数量上到几十上百之后,手工看日志还是太低效了。一个带时间线的任务流转面板,能看到每个任务经过哪些Agent、每个节点消耗多长时间、上下文交接有没有丢字段,比日志文件直接得多。这件事在工程上不难,但能极大提升运维效率。
说到底,Agent-Reach就是一句话:让Agent协作从"人盯着配管道"变成"Agent自己注册、自己交接、自己失败重试"。这条路走得并不轻松,但每次看到一条原本要人工盯半天的多Agent链路能自己跑完、还能把问题钉在某一环上报出来,我都觉得这层协调层值得做。现在它在我自己的项目里已经稳定跑了两个多月,下一阶段我打算把路由策略插件和可视化面板做出来之后,再把整体的接入门槛再降一降。