Agent-Reach 这个名字是去年我们做智能体编排压测时偶然定下来的。当时团队里已经有七八个智能体服务在线上跑各自负责日程、数据查询、文档处理、消息推送等不同职能单个拉出来都能完成特定任务但一旦让它们协作起来问题就全暴露在怎么找到对方、怎么把任务交给对方、怎么确认对方真的做完了这一层。名字里带个 Reach就是因为它本质上解决的是触达问题不只是网络能不能连通而是任务在多个智能体、多个工具之间传递时寻址、路由、确认、重试这些动作能不能顺畅完成。如果你也在做智能体编排、多模型协同或者是想把一堆分散的自动化脚本整合成一个能互相调用的体系这套设计思路和踩坑记录应该对你有用。我没有把 Agent-Reach 做成一个多么庞大的平台它就是一套轻量级的智能体触达中间层核心只有三张表、一条总线和一套接入手册。下面我把它的设计动机、实现细节和真实运行中碰到的问题都整理出来尽量做到可以直接照着落地。1. 为什么触达会成为智能体协作的第一个瓶颈很多团队一开始做智能体编排都是从直连开始的让 Agent A 在代码里直接调用 Agent B 的接口。这样做的优点是直观写两个 HTTP 请求就能跑通但智能体数量一旦超过三五个这种直连模式就会立刻变得脆弱。我在实际项目里的体会是智能体协作和普通微服务调用最大的区别在于普通服务之间的接口和依赖是相对固定的而智能体之间的调用关系是动态的、语义化的一个任务可能临时决定需要哪个能力提供方来配合这种不确定性让静态直连几乎无法维护。1.1 直连模式的三宗罪第一宗罪是寻址写死。Agent A 直接调 Agent B 的 URLB 的地址一变、部署环境一换A 就要跟着改代码。我们早期就吃过这个亏测试环境用的是带端口的内网地址上了容器编排之后内部 DNS 变了一堆调用直接超时。第二宗罪是能力信息无法复用。A 根本不知道 B 除了接口文档里写的那个功能之外还能做什么等到新的 Agent C 加入它可能具备更强的同类能力但 A 还是只会往 B 发请求完全用不上新能力。第三宗罪是故障域被无限放大。B 一旦抖动所有直接调用 B 的上游都会跟着遭殃如果 B 又去调用 C最后实际是一个连锁故障排查链路长到让人头痛。这三宗罪本质上是同一个问题智能体之间缺少一个统一触达面。所谓触达面就是所有调用方不用关心对方是谁、在哪里、怎么连只需要描述我要干什么、需要什么能力由中间层去完成匹配和转发。这个思路和我们在微服务里用注册中心做服务发现很像但智能体场景比微服务更麻烦因为能力不是简单的接口名能描述的它带语义带输入输出约束还带上下文状态。1.2 一次真实故障现场有一次我们做内部运营报表问答流程是这样的用户提问题报告 Agent 负责拆解需求然后调用数据查询 Agent 去取数取数过程中又需要调用权限校验 Agent 确认用户能不能看某些字段最后再调用一个写工单的 Agent 把异常数据记录下来。当时权限校验 Agent 的地址是写死在数据查询 Agent 的配置文件里的是个老的负载均衡入口。后来那个入口因为证书过期被回收了数据查询 Agent 调用它直接 502但它不理解 502 是对方地址失效了只会盲目超时重试链路里的其他 Agent 又都在等它返回最后整个报表链路卡了将近二十分钟才恢复。这个问题不是模型能力不够也不是某个智能体逻辑写错了就是最基础的触达没做好。那之后我们才开始认真设计这套中间层。我当时给自己定的标准很简单任何一个智能体的部署地址、接口格式、处理能力发生变化不能影响其他智能体任何一个智能体下线或者异常触达层要能立刻感知并且把流量调度到其他可用节点上任何一次调用无论成功失败都要能追溯到底。Agent-Reach 就是在这些要求下逐步长出来的。2. Agent-Reach 的三张核心表注册表、能力表、路由规则表这套中间层的整体概念可以被压缩成三张表。第一张是名称注册表解决有哪些智能体可用第二张是能力表解决它们各自能处理什么第三张是路由规则解决一个任务来了到底该交给谁。这三张表是触达层的灵魂其他一切功能都是围绕它们展开的。2.1 名称注册表先解决找得到每个智能体在启动时必须向触达服务注册自己的服务标识、节点地址、权重和标签。注册表里最常见的记录长这样字段含义示例service_id全局唯一的服务名>{ service_id: doc-summarizer, capabilities: [ { name: file_summary, description: 输入本地文件路径或文件内容输出摘要和关键要点, input: { type: object, properties: { file_path: {type: string}, content_type: {type: string, enum: [report, email, code]} }, required: [content_type] }, output: { type: object, properties: { summary: {type: string}, keywords: {type: array, items: {type: string}} } }, cost_level: medium, rate_limit: 10 } ] }能力表里最容易被忽视的是 cost_level 和 rate_limit。这两个字段在路由时非常有用如果一个任务对时延不敏感、但是量很大我们可以优先路由给 cost_level 低的节点如果一个任务必须尽快返回那我们就要避开那些已经接近 rate_limit 的节点。这一步相当于给路由加了一个成本感知的维度让触达不只会转发还会选路。2.3 路由规则人工与自动的边界能力表提供了匹配的可能但真正决定一个具体任务发给谁的是路由规则。我们把路由规则做成了一组声明式配置支持三种基本策略精确匹配、模糊匹配和加权的动态分配。规则文件长这样routes: - id: data_query_route match: capability: data_query tags: [prod] strategy: type: weighted_random weights: >services: etcd: image: quay.io/coreos/etcd:v3.5.14 command: etcd -advertise-client-urls http://etcd:2379 -listen-client-urls http://0.0.0.0:2379 ports: - 2379:2379 agent-reach: image: agent-reach:latest environment: REGISTRY_ENDPOINTS: http://etcd:2379 LISTEN_ADDR: :8080 ROUTE_FILE: /config/routes.yaml ports: - 8080:8080 depends_on: - etcd dashboard: image: agent-reach-dashboard:latest environment: REACH_API: http://agent-reach:8080 ports: - 9090:9090这套东西我建议不要在单体架构里把存储、调度和面板拆成三个独立进程。因为智能体任务往往需要长时间运行如果调度进程重启至少注册表和面板还能继续观察状态不至于整个链路全黑。4.2 智能体接入步骤注册、处理、回执智能体接入的过程我总结为三步初始化 SDK、注册功能、实现处理函数并启动。以 Python 为例接入代码的骨架如下from agent_reach import AgentClient, Capability client AgentClient( service_iddoc-summarizer, reach_addrhttp://agent-reach:8080, heartbeat_interval15, ) client.register_capability( Capability( namefile_summary, input_schema{type: object, properties: {...}}, output_schema{type: object, properties: {...}}, cost_levelmedium, ) ) client.handle(file_summary) async def handle_file_summary(payload, ctx): # 这里写具体的智能体逻辑 result await do_summary(payload[path]) return {summary: result.summary, keywords: result.keywords} client.start()这里有几个细节你第一次写时很容易错。第一注册能力时填的 input_schema 必须和实际处理函数对得上否则触达层可能把一个根本不符合 schema 的任务转发过来处理函数直接解析失败。第二handle 函数里一定要接收 ctx因为 ctx 里带着 call_id、deadline 等信息你的内部日志、调用外部系统的 trace_id 都需要跟 ctx 里的 call_id 串起来不然出问题的时候根本没法追踪。第三心跳间隔不要太长也不要太短15 秒是一个比较合理的值。间隔太长节点挂掉不能被及时感知间隔太短每个节点的心跳请求会对触达层造成无意义压力。启动以后智能体会周期性地把注册信息上报到触达层。如果注册表里已经能看到服务名和节点下一步就可以测试一次简单的触达调用了。4.3 一个综合配置示例和调整思路我再给一个相对完整的路由配置示例把前面提到的几种策略串起来routes: - id: summary_main_route match: capability: file_summary input.content_type: report strategy: type: latency_aware services: [doc-summarizer, llm-light] priority: 90 - id: summary_general_route match: capability: file_summary strategy: type: weighted_random weights: doc-summarizer: 40 llm-light: 60 priority: 50 fallback_rules: - condition: no_available_node action: return_503 wait_before_retry_ms: 500路由规则的 priority 是另一个很容易踩坑的点。我们一开始没设置优先级结果按顺序匹配的时候一条通用规则把特异性强的任务提前抢走了导致某些文件走了错误的处理链路。后来我们明确优先维度越细、条件越多的规则优先级越高。这跟防火墙规则的设计思路很像具体规则永远排在兜底规则前面。配置文件修改后不需要重启调度进程走动态加载。不过动态加载有个坏处规则变更太频繁容易让线上行为难以预期。我们内部约定路由规则的变更必须走一次评审至少要有一个人确认改变后的影响范围。这是运维纪律的问题跟技术实现关系不大但我觉得有必要在这里说清楚。5. 实测碰到的四个坑与完整排查链路这部分是我不想藏私的内容。Agent-Reach 上线后我们在灰度期和稳定运行期踩过不少坑有些坑非常隐蔽我想把完整的排查链路写出来方便大家复现和规避。5.1 坑一能力声明过宽引发路由风暴最初我们给一个通用文本处理智能体注册了很多能力比如文本总结、情感分析、语言翻译、关键词抽取而且每项能力的 input_schema 都写得比较宽松比如允许传入任意字符串。这不是为了偷懒而是我们希望这个智能体什么都能做尽量多接任务。结果上线后发现触达层几乎把所有文本类任务都路由给了它而其他一些专业智能体完全闲着。更严重的是这个通用智能体的处理时延很高任务一多就开始排队排队超过超时时间后部分任务被重试路由重试又打到同一批节点形成了恶性循环。排查思路是这样的我们先把触达层的路由命中日志拉出来按 service_id 聚合发现通用文本处理的命中率占了 80% 以上接着对比它的能力声明和其他智能体的声明发现通用文本处理的能力 schema 描述覆盖了其他智能体的所有输入类型。根因很清楚schema 描述的自由度太高导致规则匹配时优先级没有拉开差距。修复方法是给每项能力声明增加明确的特征字段和互斥约束比如情感分析必须要求输入里有text和target_lang两个字段并且text长度限制在 2000 字以内。这样能避免一个任务同时匹配多个能力。5.2 坑二重试风暴差点打挂执行单元第二个坑是我们自己种下的。当时为了追求高可用我们对所有失败任务执行最多 5 次重试指数退避设置得也比较激进结果在某次上游批量推送任务时因为下游一个节点重启大量任务在短时间内连续失败。失败后重试重试又撞上同一个节点重启造成重试风暴整个执行单元 CPU 被打到 90% 以上正常任务也进不来了。排查时我们做了三件事第一检查触达层的重试日志发现同一条 idempotency_key 在 1 分钟内被复制出了 6 条记录第二对比下游节点的健康状态发现它在这段时间里处于进程活着但任务处理超时的半死不活状态第三我们确认了问题不在网络层而是下游的处理线程池被占满了。修复分两个层面。一是把重试策略从最大重试次数 5 次改成总耗时不超过 30 秒期间最多重试 2 次并且每次重试前计算retry_base_delay_ms乘以随机因子让重试分布更均匀。二是在触达层加了一个简单的熔断器如果某个 service_id 的错误率在 10 秒窗口内超过 50%直接熔断 5 秒不再往该节点转发任何任务。熔断的意义不是解决故障本身而是防止故障被无限放大给下游恢复争取时间。5.3 坑三长上下文任务的超时时间设置处处别扭智能体不同于普通 API很多任务非常耗时。比如让一个智能体阅读一个 50 页 PDF 然后总结光读取解析可能就要几十秒如果再涉及多轮推理一分钟以上很正常。我们一开始设置的执行超时是 20 秒导致很多长任务被触达层误判为失败白白消耗了计算资源。排查过程很典型回执数据里出现大量 timeout 错误但查看智能体日志发现任务其实一直在处理中。当时我们先怀疑是心跳回执没生效检查后发现心跳是正常的问题就出在硬性超时上。这里我的建议是把超时按任务复杂度拆档位轻量任务 10 秒中等任务 60 秒重量任务 10 分钟。并且执行节点必须在处理过程中持续发送心跳回执否则触达层依然会按最终超时判断。另外一个更隐蔽的问题代码里所有执行节点必须在收到 deadline_millis 之后用自己的本地时间算一个本地截止时刻如果发送方和接收方存在时钟偏差就可能导致任务还没开始就被判定过期。解决办法是触达层在信封上附带一个转发时戳执行节点以这个时戳为准做倒计时而不是完全依赖本地时钟。5.4 坑四健康检查不完善活着的节点被摘掉最后一个坑是关于健康检查的。智能体节点的进程可能活得很好但它依赖的模型推理服务已经卡住了这时候如果触达层只检查进程端口它依然会把任务发过来导致大量失败。更麻烦的是反向情况某些节点的健康检查太激进检查一次失败就把节点摘掉结果节点恢复之后好几分钟没有被重新加入路由出现大段真空期。我们最终的方案是把健康检查分成两层Liveness Check 和 Ready Check。Liveness 检查只确认进程还在、端口还在监听Ready Check 则用最小的探针任务去验证节点能否真正处理一个零成本任务比如返回当前时间。只有 Ready Check 通过的节点才会被放进路由池。探针频率不能太高我建议 10 秒一次同时探针不能和正常业务任务抢资源要给探针打上最低优先级。6. 这套设计的实际适用边界与个人体验最后说点偏向个人判断的内容。Agent-Reach 不是银弹它解决的是智能体数量多了之后怎么协作的问题但并不是所有协作场景都需要这么重的一层中间件。我的经验是如果智能体数量在五六个以下而且彼此调用关系固定那还不如用最朴素的直连加一个公共配置中心管理地址比引入一套触达层简单得多。只有当调用关系存在明显的动态性比如一个任务的执行链路可能需要临时选择能力提供方、智能体节点经常上下线、或者你需要精细的灰度路由和全链路审计时Agent-Reach 这类设计才真正值得投入。我在实际部署中最大的体会是触达这件事的价值不在于转发本身而在于它把智能体之间的协作从点对点契约转变成了注册与声明。你不需要在代码里关心对方是谁、部署在哪、能力如何只需要声明自己能做什么、需要什么剩下的事情交给触达层。这个思维转变带来的维护成本下降比任何具体技术都要有价值。如果让我给后来者一个最核心的建议那就是别急着把路由规则做得很复杂。先把名称注册表和回执机制这两件事做扎实让所有智能体能被找到、调用过程能被看到就足够解决大多数协作问题了。等到真的出现路由选择不合理、并发控制不精准这些高级问题时再逐步把能力表和路由规则的细节补上你会比我当初少走很多弯路。