
做多智能体系统大半年踩过不少坑。最让我头疼的不是单个Agent的推理效果而是多个Agent之间怎么互相发现、怎么把任务准确送达到对的Agent手里、以及失败之后怎么快速定位。后来我把内部这套方案整理成一个可复用组件取名 Agent-Reach主要解决的就是“多智能体触达与编排”的最后一公里。它不是一个花哨的框架而是一套轻量、可插拔的中间层让不同技术栈、不同团队维护的Agent能够像服务一样注册、被发现并按语义能力被路由调用。如果你正在做多智能体应用、工作流自动化或者准备把已有的AI能力拆成可协作的Agent集群这篇文章可以给你一个可以直接上手的参考。1. 多智能体系统里的“触达难题”到底是什么1.1 传统API调用模式为什么撑不起Agent协作先回想一下常见的单体应用服务之间通过REST或RPC调用接口路径、参数、鉴权方式都是预先约定好的。这种模式在人工开发、需求明确的场景下没有问题但一旦参与者变成了AI Agent事情就变了。Agent不像传统服务那样只有一个固定输入输出它更接近一个“有理解能力的执行者”同一个Agent可能支持多种意图比如“总结文档”和“提取关键词”是同一个Agent的两种能力而不是两个独立服务。如果仍然用写死API的方式去编排Agent你的主流程里会出现大量硬编码A调用BB再判断要不要调用C每新增一个Agent主流程就要改一版。更麻烦的是Agent返回的结果经常不是标准JSON而是自然语言、半结构化文本或者工具调用片段。你要在这些输出里判断“它是不是真的完成了任务”然后再决定下一步触达谁。这种情况下传统API调用模式就成了Collaboration的瓶颈。1.2 Agent-Reach的核心思路注册、发现、路由、触达Agent-Reach把问题收敛成四件事注册、发现、路由、触达。每个Agent启动时先到Registry里登记自己的身份和能力清单其他Agent或上层编排器需要某个能力时向Reach层发起一次语义请求Reach层根据能力声明和当前状态选出最合适的Agent再把任务投递过去。投递过程统一走消息通道而不是每个调用方各自直连。这套思路很像微服务里的服务注册与发现但有一个本质差异服务发现匹配的是“接口名版本号”而Agent-Reach匹配的是“语义能力上下文约束”。例如服务发现会找“get_order_detail”Agent-Reach则可能匹配“查询订单状态”这个意图然后由路由模块自己决定是哪个Agent、哪个具体工具能完成。这个差异决定了Agent-Reach不能只抄注册中心的作业它还需要一套意图映射和路由决策逻辑。2. Agent-Reach核心架构与设计选型2.1 Agent Registry不只是存地址还得存能力画像Registry是Agent-Reach的地基。它保存每个Agent的标识、运行地址、支持的能力列表、输入输出约束、健康状态和元信息。为了支持语义匹配能力的描述不能是一个短字符串我建议用类似这样的结构agent: id: summary-agent-01 name: 文档总结Agent transport: grpc endpoint: 10.20.30.40:9091 capabilities: - name: text.summarize description: 对输入文本生成摘要支持中文和英文 input_schema: text: string max_length: integer, optional output_schema: summary: string tokens_saved: integer constraints: max_concurrency: 5 max_input_chars: 20000 preferred_batch_size: 10Registry在存储上不需要上什么重型中间件初期用PostgreSQL就够了把能力画像存成JSONB字段方便后续做灵活的查询与过滤。Agent上线时调用Register接口把这份YAML提交进来下线时调用Deregister。每次注册都会带上一个递增版本号这样路由层做灰度时不至于混乱。这里有个值得注意的细节不要只把Registry当成“地址簿”否则后续做路由调度时你会发现信息不够用。我在设计时额外要求每个Agent必须声明“不擅长什么”比如文档总结Agent明确标注不支持表格OCR。这类负向声明在路由匹配时能大幅减少误触达帮你把明显不合适的候选提前排除掉。2.2 从“找服务”到“找能力”语义路由的落地方式路由是Agent-Reach里最核心、也最容易做得过度复杂的一块。我最初的方案是直接让一个大模型来做意图识别把用户的自然语言请求映射到Agent能力上。但实际跑下来发现纯LLM判定的延迟和稳定性都不如预期而且每次路由都调一次大模型成本也不小。后来我采用了两级路由第一级是轻量规则与向量匹配第二级才让大模型处理模糊情况。具体流程是请求进来后先对输入文本做意图归一化把同义词、简写映射到能力标签上然后在Registry里做倒排索引匹配命中唯一候选就直接投递命中多个候选按排序策略选一个如果一级匹配置信度低于阈值再进入LLM路由让模型结合Agent能力描述判断最佳目标。这种设计带来的收益很直接80%的常规请求走规则和向量匹配响应时间控制在10毫秒内只有真正模棱两可的请求才需要模型参与。用大模型做兜底而不是主力才是性价比最高的选择。2.3 触达通道统一消息层与传输适配确定要把任务投给哪个Agent之后剩下的问题就是“怎么安全地把任务送过去”。Agent-Reach的触达通道设计成可插拔传输层内置支持gRPC和HTTP/WebSocket也预留了消息队列接口。每个Agent只要实现TransportAdapter就可以接入自己的通信协议。这里我建议优先考虑gRPC而不是纯HTTP。原因有两个一是Agent之间传递的结构化数据比如任务上下文、工具调用记录、分片结果用Protobuf定义会比JSON更省流量、解析更快二是gRPC天然支持双向流遇到需要持续返回中间结果的Agent比如流式读取日志再总结处理起来比HTTP轮询舒服得多。不过HTTP也没有被完全抛弃。对于外部系统临时触达的请求比如Webhook触发一个Agent直接用HTTP接口暴露更简单。我在Agent-Reach里做了一个转换层把外部HTTP请求统一翻译成内部消息再投递给gRPC通道调用方完全感知不到后端用的是流式还是单工。3. 实操十分钟跑通一个多Agent协作场景3.1 环境准备与启动Reach核心服务先准备好基础环境。以我常用的Docker Compose方式举例需要三个容器PostgreSQL、Reach核心服务、一个示例Agent。下面是一份最小可用的docker-compose文件省去镜像细节version: 3.8 services: postgres: image: postgres:15 environment: POSTGRES_DB: reach POSTGRES_USER: reach POSTGRES_PASSWORD: reach123 ports: - 5432:5432 reach-server: image: reach-server:latest environment: DB_DSN: postgres://reach:reach123postgres:5432/reach?sslmodedisable ROUTER_MODE: hybrid LLM_ENDPOINT: http://llm-service:8000/v1 ports: - 8080:8080 depends_on: - postgres sample-agent: image: sample-agent:latest environment: REACH_REGISTRY: http://reach-server:8080/registry AGENT_ID: summary-agent-01 depends_on: - reach-server启动后Reach核心服务会暴露三个入口/registry用于注册发现/route用于任务路由/admin用于管理端查询Agent状态。先用健康检查接口确认所有组件起来curl http://localhost:8080/health返回{status:ok}就说明核心服务就绪了。3.2 注册一个Agent并声明能力Agent启动时会自动向Registry提交能力画像。在示例Agent里我预置了文档总结和关键词抽取两个能力。注册完成后可以通过管理端接口确认curl http://localhost:8080/admin/agents/summary-agent-01正常返回里会包含capabilities数组以及last_heartbeat字段。这里有个容易踩的坑Agent的last_heartbeat如果超过30秒没更新Reach会认为它离线并把它的路由权重降为0。所以Agent端要写一个后台心跳任务建议每10秒上报一次。用一段伪代码演示import time import requests while True: requests.post( http://reach-server:8080/registry/heartbeat, json{agent_id: summary-agent-01} ) time.sleep(10)3.3 发起一次跨Agent调用现在模拟一个真实场景用户输入一段新闻稿要求“总结一下这段文本并提取里面的公司名称”。这个任务涉及两个Agent一个做总结一个做实体抽取。用Agent-Reach的Python客户端发起请求from agent_reach import ReachClient client ReachClient(base_urlhttp://localhost:8080) response client.invoke( intenttext summarize and extract organizations, payload{ text: 某科技公司今日宣布完成新一轮融资投资方包括多家机构。, }, fallback_policywait_for_any, timeout30, ) print(response.task_id) print(response.result)Reach内部会把这个请求拆成两个子任务分别路由到summary-agent和extract-agent最后在编排层做结果合并。这里的关键参数是fallback_policy如果summary-agent无响应是等另一个Agent成功还是直接失败返回需要在业务层面权衡。想查看任务链路可以用管理端接口拉取curl http://localhost:8080/admin/tasks/{task_id}/trace返回结果里会记录每一步的触达时间、目标Agent、耗时和状态排障时非常有用。3.4 关键配置项说明路由模式、超时与重试实际部署时有几个配置项直接影响可用性我整理在下面配置项可选值建议值说明ROUTER_MODErule / vector / hybridhybridhybrid先用规则和向量再LLM兜底兼顾速度与准确率ROUTER_LLM_THRESHOLD0.0 ~ 1.00.65一级匹配置信度低于该值时进入LLM路由TASK_TIMEOUT毫秒30000单任务最大执行时间超过则标记失败并触发补偿MAX_RETRY整数2失败重试次数重试时排除上一次的失败节点HEARTBEAT_WINDOW秒30Agent心跳超时窗口影响路由权重MAX_INFLIGHT整数500Reach核心同时处理的任务数用来做背压控制我建议在项目初期就把这些参数纳入统一的配置中心管理而不是散在各个环境变量里。因为调优时经常要同时修改几个参数比如提高ROUTER_LLM_THRESHOLD的同时也要降低TASK_TIMEOUT否则LLM兜底路径可能拖慢整体响应。4. 实战踩坑记录与调优心得4.1 我遇到过的三个典型问题第一个问题是“Agent注册成功但路由不到”。排查后发现是能力标签不统一惹的祸用户请求里说的是“提取公司名”Agent能力声明里写的是“org_extract”两者没有关联。后来我在Registry里增加了aliases字段让每个能力可以声明同义词同时在向量匹配层做了别名扩展这个问题才彻底消失。第二个问题是“任务投递成功但Agent不消费”。现象是Reach侧显示消息已发送但目标Agent迟迟不处理。定位到最后原因是目标Agent基于HTTP长轮询拉取消息而Reach默认用gRPC推送两边协议没对上。所以前期规划一定先确认每个Agent的消费方式不能一股脑默认全gRPC。第三个问题是“LLM路由兜底经常超时”。一开始我把超时设成5秒但大模型接口偶尔要8秒才能返回导致很多可正确路由的任务被判失败。后来我把LLM调用改成异步预加载提前把常见意图的Embedding缓存到内存大部分请求在缓存里就能命中只有真正没见过的请求才走在线模型超时问题就缓解了很多。4.2 路由策略与超时控制的调优笔记调优最有价值的一个改动是把“全局超时”改成“分阶段超时”。一个完整任务可能包含路由、投递、执行、结果合并四个阶段每个阶段的耗时特征完全不一样。如果所有阶段共享一个30秒超时很可能某个Agent已经执行成功但结果在合并阶段慢了一会儿整个任务被误杀。我的做法是在任务上下文里记录每个阶段预算例如路由3秒、投递5秒、执行20秒、合并2秒。每个阶段开始时检查剩余预算超了就立刻终止并返回清晰错误码。这样排障时能直接看到是哪个阶段耗尽了预算而不是只看到“Timeout”两个字。另一个调优点是重试策略。默认重试同一个Agent往往会再次失败不如换成“排除式重试”第一次失败后把该Agent加入黑名单重试时只从其他候选Agent里选。比如有两个Agent都能做摘要第一个超时后自动切换到第二个成功率会明显提升。4.3 可观测性实践日志、链路追踪与状态检查Agent-Reach初期我根本没加链路追踪出了问题只能靠各Agent自己打日志非常痛苦。后来引入了OpenTelemetry标准把每个跨Agent调用都打成一个Span包含agent_id、capability、task_id、阶段耗时等标签。这样在Jaeger里能看到完整调用链定位慢节点变得很直接。日志上我统一了结构化格式每行日志必须包含task_id和agent_id两个字段。举一个具体例子{ time: 2024-11-20T10:15:30.123Z, level: WARN, task_id: task-5f3a1c, agent_id: summary-agent-01, event: route.low_confidence, confidence: 0.42, fallback: llm_router }这种日志看起来多几行但在排查“为什么这个任务被路由到A而不是B”时比什么“路由失败”四个字有用得多。建议从第一天就统一日志字段后续加分析工具会省很多功夫。5. 生产落地建议与扩展方向5.1 什么场景适合Agent-Reach什么场景要慎重Agent-Reach最适合的场景是Agent数量在三个以上、能力边界经常变化、需要跨团队协作或者任务链路会动态调整。比如客服中心把质检、摘要、工单分类、知识库检索拆成独立Agent由Reach统一调度这种模式能明显降低各Agent之间的耦合度。但如果你只有一两个固定Agent或者任务流程完全固定现阶段我反而不建议上Agent-Reach。引入它意味着多维护一套注册中心、一套路由逻辑和一套可观测体系短期内比硬编码调用链更重。要清醒地认识到中间件解决的是“动态性和协作成本”问题不是所有系统都需要这种灵活性。5.2 与LLM、消息队列和工作流引擎的配合方式Agent-Reach并不排斥LLM它只是把LLM当作能力提供方。你可以把一个带工具调用的ReAct Agent注册进来也可以把“意图识别服务”作为路由模块的兜底节点。在Agent内部该用LangChain还是直接调OpenAI SDK都由Agent自己决定Reach只关心它能否以统一格式接收任务并返回结果。与消息队列配合时一个常见做法是Reach负责路由决策并把任务写入KafkaAgent从Kafka消费处理处理结果再写回结果Topic。这样即使Agent崩溃重启消息也不会丢失。如果追求更简单的同步调用可以让Reach直接投递gRPC请求但需要自己处理Agent重启期间的未完成任务。工作流引擎则可以放在Reach上层由工作流引擎定义业务流程比如先总结再提取Reach负责每次流程节点的具体Agent选择。两者各司其职避免把流程编排逻辑硬塞进路由组件。5.3 未来扩展Agent市场、联邦触达与语义缓存Agent-Reach按目前架构继续发展有几个很自然的扩展方向。一是Agent市场在Registry之上做一套能力订阅和评分机制让团队可以发布自己的Agent能力其他团队按需订阅使用本质上变成一个组织内部的“AI能力商店”。二是联邦触达多个Reach实例之间通过安全协议交换能力摘要实现跨部门、跨机房的Agent互访但每个实例依然保有自治权。三是语义缓存对于高频且结果稳定的请求直接把“意图输入摘要输出结果”缓存起来下次不再重复调度Agent能显著降低成本和延迟。这三个方向里语义缓存最容易先落地。我现在已经在高复用场景里试跑一版命中率大概在30%左右已经能省掉不少重复计算。等缓存命中质量稳定后再考虑开放给更多场景。最后聊几句实际操作中的体会从最初在本地写死调用链到现在用Agent-Reach统一编排给我的最大感受不是“框架多厉害”而是“把触达层显式化之后整个系统的边界清晰了”。以前每个Agent都在猜下一个该调谁现在它们只需要告诉Reach自己能做什么剩下的事情交给路由层。踩过几次坑之后我建议所有准备做多智能体系统的朋友先不要急着上复杂调度框架哪怕只是用注册表简单路由先搭一个雏形也比直接硬编码调用链更容易演进。Agent-Reach现在还在持续迭代如果你也在做类似的东西欢迎围绕触达、路由、可观测性这几个点多交流。