
如果你手头已经有五个以上Agent在跑生产任务你迟早会撞上那个既尴尬又现实的问题它们之间根本不认识。我见过不少团队花大力气让Agent写周报、做数据分析、自动回工单最后却被“怎么让Agent A把结果递给Agent B”这种基础问题卡了半个月。Agent-Reach就是冲着这个痛点来的——它不是某个大模型更不是一套UI而是一层让Agent能够互相发现、安全寻址、按能力路由消息的通信协议与节点中间件。简单说Agent-Reach解决的是Agent生态的“通讯录”和“邮局”问题。它的使用场景很明确你希望不同类型的Agent不管是用LangChain搭的还是AutoGen跑的还是自己纯手写的能在一个网络内互相协作而不是靠人肉逐个对接API。适合所有已经在批量使用Agent、准备做多Agent协作、或者觉得“Agent越多越难管”的开发者阅读。本文将围绕Agent-Reach做完整拆解从它要解决的问题、三层协议架构到实际搭建节点、接入Agent、跑通跨Agent协作的完整链路再到压测调优、避坑经验全程按我这些年的实操经历来写不会只给你看概念。文中的配置和代码都能直接抄前提是你先把网络环境准备清楚。1. 碎片化的Agent生态为什么每个团队最终都需要“碰头”层先说个我自己的观察。现在的Agent项目看起来百花齐放实际上默认处于“孤儿模式”。你在订单系统那边跑着几个负责客服的Agent在数据平台这边跑着几个做报表解读的Agent它们各自有各自的知识库和工具调用方式但要让它们协作你会发现只能用最原始的办法把A的输出存成文件再让B去读或者写一堆HTTP回调把每个Agent都当成独立Web服务来调。这套打法的毛病我用三条给你说透。1.1 接口耦合把人拖进了体力活每个Agent对外暴露的接口风格都不一样。有的给你OpenAPI规范有的就是简单POST一个JSON还有的干脆只能通过CLI传参。你要做多Agent协作光处理这些接口差异就够写几百行胶水代码。更麻烦的是每次新加一个Agent所有依赖它的调用方都要跟着改。这种强耦合关系在Agent数量超过十个之后基本就是维护噩梦。1.2 Agent没有“按能力寻址”的机制HTTP世界里有网关和注册中心但Agent世界里很少有人主动给自己的Agent建立索引。于是你想调一个“能处理PDF转Markdown”的Agent只能在代码里写死它的URL或内网IP。它哪天升级换了端口、换了机器整条链路就断了。真正的Agent协作应该像人用通讯录一样我需要某个能力找到那个能力把任务发过去至于它部署在哪台机器、用什么框架实现调用方不需要关心。1.3 安全模型缺失谁也信不过谁自己做HTTP调用的时候最常见的做法是给每个服务发一个静态Token写在配置文件里。Agent多了之后Token管理和轮换就是灾难。一个Agent被攻破所有Token都可能泄露。更关键的是Agent之间本质上需要的是“身份”和“权限策略”而不是一个永久有效的密钥。这就逼着你在做协作之前先把信任体系做出来。Agent-Reach的切入角度就是把上面三个问题统一解决它定义了一套稳定的Reach-ID作为Agent身份标识提供注册中心用来按能力发现Agent再用基于公钥指纹和策略的鉴权机制做消息路由。我们在生产里跑了一年这套设计能撑住是因为三层架构各管各的事没有把简单问题复杂化。2. Agent-Reach核心架构拆解从Reach-ID到一条消息的完整旅程我直接给你看Agent-Reach的架构设计这是理解后面所有实战操作的基础。它整体分三层层次不堆叠职责边界非常清楚。2.1 传输层、路由层与应用层的分工我用一个通俗的比喻传输层是公路路由层是交通指挥中心应用层是你车上装的货。传输层Transport负责管连接。Agent-Reach默认走TCP长连接底层用固定长度的帧头加JSON负载的封装方式避免粘包拆包的麻烦。也支持WebSocket方便浏览器场景下的Agent接入。路由层Routing以ReachHub为核心。每个Agent启动后向ReachHub注册自己的Reach-ID、所属命名空间、能力列表Capabilities和当前可达的endpoint。Agent之间不直接保存对方的地址而是每次需要通信时通过ReachHub做“寻址”或“间接转发”。应用层Application跑Reach Message ProtocolRMP。这是一套类JSON-RPC的协议包含discover、register、send、invoke四个核心原语。业务方平时的调用逻辑全部在这一层完成不用关心底层连接怎么维护。这三层完全解耦之后最大的好处是你可以换掉传输层改用QUIC或者替换ReachHub的存储实现而Agent的业务代码一行都不用动。2.2 Reach-ID的绑定逻辑用公钥指纹做身份锚点Reach-ID的格式是reach://namespace/agent-name比如reach://prod/order-analysis reach://internal/translator看起来像URL但它的核心不在命名而在注册时通过非对称加密生成的密钥对。每个Agent在首次初始化时都会生成一个Ed25519密钥对私钥留在本地公钥上传到ReachHub。ReachHub会把该Agent的Reach-ID与公钥指纹取公钥SHA-256哈希的前16字节转十六进制做一对一绑定。这意味着什么意味着身份验证不再依赖Token而是每次消息都带签名。接收方收到消息后用发送方公钥验签指纹对上了才能确认“这个消息真的是这个Reach-ID发出来的”。我在实测中发现这套机制天然支持密钥轮换——只要Agent本地重新生成密钥对并更新注册信息旧签名立刻失效不存在Token泄露后手动吊销多个服务的噩梦。2.3 一条消息从Agent A到Agent B的完整路径我把完整流程画成可执行的步骤你可以照着理解Agent A先做本地缓存查询看有没有B的endpoint缓存。有就直接走传输层发送没有就进入下一步。Agent A向ReachHub发起resolve请求携带B的Reach-ID。ReachHub校验A的签名确认有读取B所属命名空间的权限然后返回B的endpoint列表以及B的公钥。Agent A拿到endpoint后构造RMP消息用A的私钥签名发送给B。Agent B收到消息先验签再校验A是否在该服务允许的调用方列表中。通过则进入业务逻辑返回结果同样签名。这里有个细节值得注意ReachHub不转发全量消息内容只负责发现和返回路由信息。真正的内容传输是Agent之间点对点的。这跟传统API网关那种“所有流量过网关”的模式有本质区别内网带宽压力小很多消息延时也更低。3. 从零搭建Agent-Reach节点环境、依赖与首次注册这一节直接进入可操作环节。我默认你在Linux服务器上实验Ubuntu 22.04最佳Windows也能跑但别在生产环境这么干。3.1 环境准备与依赖安装Agent-Reach的服务端ReachHub我用Python实现因为生态成熟、asyncio在处理长连接时非常省心。你需要准备Python 3.10及以上SQLite 3.37以上ReachHub默认存储内网至少两台互相能通的机器一台跑ReachHub一台跑Agent依赖库很少核心就三个pip install fastapi uvicorn pydantic pynacl注意PyNaCl是用来做Ed25519签名验签的。不要自己写加密逻辑Agent通信里的签名验签必须走正规密码学库。3.2 初始化ReachHub注册中心ReachHub的迷你实现并不复杂核心是一个FastAPI服务加SQLite表。我把关键的初始化和注册接口代码贴出来这是你整个Agent-Reach网络的“神经中枢”。# hub.py from fastapi import FastAPI, HTTPException from pydantic import BaseModel import sqlite3, hashlib, time app FastAPI() def init_db(): conn sqlite3.connect(reachhub.db) c conn.cursor() c.execute( CREATE TABLE IF NOT EXISTS agents ( reach_id TEXT PRIMARY KEY, namespace TEXT NOT NULL, endpoint TEXT NOT NULL, capabilities TEXT, pubkey_fingerprint TEXT NOT NULL, status TEXT DEFAULT online, updated_at INTEGER ) ) conn.commit() conn.close() class RegisterReq(BaseModel): reach_id: str endpoint: str capabilities: list[str] pubkey: str app.post(/register) def register(req: RegisterReq): # 计算公钥指纹 raw bytes.fromhex(req.pubkey) finger hashlib.sha256(raw).hexdigest()[:16] conn sqlite3.connect(reachhub.db) c conn.cursor() c.execute( INSERT INTO agents (reach_id, namespace, endpoint, capabilities, pubkey_fingerprint, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(reach_id) DO UPDATE SET endpointexcluded.endpoint, capabilitiesexcluded.capabilities, pubkey_fingerprintexcluded.pubkey_fingerprint, statusonline, updated_atexcluded.updated_at , (reach://prod/order-analysis, prod, req.endpoint, ,.join(req.capabilities), finger, int(time.time()))) conn.commit() conn.close() return {status: registered, fingerprint: finger} # 简易运行入口 if __name__ __main__: init_db() import uvicorn uvicorn.run(app, host0.0.0.0, port8920)启动方式python hub.pyReachHub默认监听8920端口这个端口你可以自己定但要在所有Agent节点的配置里保持一致。我用SQLite是图省事实际生产建议把存储换成PostgreSQL尤其是Agent数量超过五百之后SQLite的写锁会开始在注册/心跳阶段成为瓶颈。3.3 接入一个Agent并完成首次注册Agent端我封装了一个轻量SDK核心逻辑就是建连接、维护密钥、处理心跳。初始化代码如下# agent_sdk.py import asyncio, json, hashlib from nacl.signing import SigningKey import aiohttp class ReachAgent: def __init__(self, reach_id, hub_url, capabilities, endpoint): self.reach_id reach_id self.hub_url hub_url.strip(/) self.capabilities capabilities self.endpoint endpoint self.sk SigningKey.generate() self.vk self.sk.verify_key async def register(self): pubkey_hex self.vk.encode().hex() payload { reach_id: self.reach_id, endpoint: self.endpoint, capabilities: self.capabilities, pubkey: pubkey_hex, } async with aiohttp.ClientSession() as session: async with session.post(f{self.hub_url}/register, jsonpayload) as resp: return await resp.json()调用注册import asyncio async def main(): agent ReachAgent( reach_idreach://prod/order-analysis, hub_urlhttp://hub.internal:8920, capabilities[order.analyze, order.report], endpointtcp://agent-01.internal:9102 ) result await agent.register() print(注册结果:, result) asyncio.run(main())这里有个容易踩的坑endpoint字段我故意写成tcp://agent-01.internal:9102这种格式因为Agent-Reach传输层不止支持HTTP。如果你跟我一样后续用TCP长连接做点对点通信endpoint的scheme需要区分清楚。前期调试阶段这里可以直接填当前Agent的HTTP回调地址比如http://agent-01.internal:9102/receive消息路由层会以HTTP POST方式推送过去方便你先跑通链路再优化传输层。4. 核心接口使用详解发现、寻址、消息路由与鉴权策略注册只是第一步Agent-Reach真正值钱的是后面那套发现和路由机制。这节我把用得最频繁的四个接口逐个拆开每个都会给到调用方式、返回结构和我的实践经验。4.1 按能力发现Agent而不是按名字实践中你通常不知道某个服务由哪个Agent负责你只知道自己想要什么能力。Agent-Reach的discover接口支持按能力查询curl -X POST http://hub.internal:8920/discover \ -H Content-Type: application/json \ -d {capability: order.analyze}返回结构{ results: [ { reach_id: reach://prod/order-analysis, endpoint: tcp://agent-01.internal:9102, pubkey_fingerprint: 9f2c8e1a7b3d5f60 } ] }我设计discover时特意把业务Agent的“表达能力”跟“寻址信息”分开了。查询只用capability字段响应里也不会带完整公钥那等于把敏感信息暴露给所有调用方只返回指纹。需要完整公钥验签的走resolve接口拿。这个细节让信息泄露面小了很多。4.2 resolve寻址调用方到目标的最后一步发起消息前调用方需要先拿一次endpoint和公钥的完整信息curl -X POST http://hub.internal:8920/resolve \ -H Content-Type: application/json \ -d {target: reach://prod/order-analysis}返回{ reach_id: reach://prod/order-analysis, endpoint: tcp://agent-01.internal:9102, pubkey: 1a2b3c...完整十六进制公钥 }拿到这个就能开始发消息了。这里有一个我在多次压测后总结的规律resolve结果一定要在调用方本地缓存缓存命中率直接决定整套系统的延迟上限。每次发消息都来ReachHub查一次Agent数量上来之后ReachHub会先成为瓶颈。标准做法是本地缓存resolve结果至少60秒如果消息发送失败再回头重新resolve而不是一开始就全量依赖注册中心。4.3 send发送与invoke远程调用签名验签的实际应用send是纯粹把一条消息投递给目标Agent不关心有没有回复。invoke是同步远程调用会等待目标返回结果。两者在签名流程上没有区别。核心代码# 在ReachAgent类中补充发送逻辑 import aiohttp, time async def send(self, target_reach_id, message: dict): msg_bytes json.dumps({ from: self.reach_id, to: target_reach_id, payload: message, ts: int(time.time()) }).encode() signed self.sk.sign(msg_bytes) payload { from: self.reach_id, to: target_reach_id, payload: message, signature: signed.signature.hex(), pubkey: self.vk.encode().hex() } # 实际发送时需要使用目标endpoint地址这里需要先resolve # 省略HTTP/TCP发送部分核心是payload中包含签名 return payload接收端的验签逻辑是安全的关键我在生产里见过太多把验签做成“可选”的案例那等于没有身份体系。验签必须强制在业务逻辑之前完成验签失败的请求直接丢弃并记日志。4.4 基于策略的访问控制比Token更接近“权限”的本质ReachHub里维护一张访问控制表核心是“某个调用方Reach-ID可以对目标Reach-ID的哪些能力发起调用”。这个策略我建议在注册时一并提交这样新Agent上线自动完成授权配置不需要运维手工在控制台上点点点。配置大概长这样{ caller: reach://prod/orchestrator, target: reach://prod/order-analysis, allowed: [order.analyze] }这种设计比单一Token更安全的地方在于即使某个Agent的私钥泄露攻击者拿着它的Reach-ID也只能调用它原本被授权的服务没办法越权。Agent被攻破不等于整个协作网络沦陷这是分布式系统里最重要的安全假设。5. 实测完整链路两个Agent协作处理订单文本分析理论说再多不如跑一遍。我搭一个最典型的场景一个orchestrator Agent负责接收原始文本另一个analysis Agent负责做订单文本的情感分析和摘要。orchestrator通过Agent-Reach发现analysis的能力然后invoke调用拿到结果后返回上层。5.1 场景设定与Agent能力规划我建了两个AgentAgentReach-ID能力职责编排者reach://prod/orchestratororchestrate.order接收外部输入分发任务分析者reach://prod/order-analysisorder.analyze, order.report执行订单文本分析两个Agent和ReachHub都跑在同一台测试机上端口互不冲突。实际生产环境ReachHub单独一台Agent分布在不同机器上。5.2 运行ReachHub并注册两个Agent先启动ReachHub然后用我前面写的ReachAgent类去做注册。我写一个完整的脚本# setup_agents.py import asyncio from agent_sdk import ReachAgent async def main(): hub_url http://127.0.0.1:8920 orchestrator ReachAgent( reach_idreach://prod/orchestrator, hub_urlhub_url, capabilities[orchestrate.order], endpointhttp://127.0.0.1:9101/receive ) analysis ReachAgent( reach_idreach://prod/order-analysis, hub_urlhub_url, capabilities[order.analyze, order.report], endpointhttp://127.0.0.1:9102/receive ) r1 await orchestrator.register() r2 await analysis.register() print(orchestrator:, r1) print(analysis:, r2) asyncio.run(main())运行后你会看到两条注册成功的日志输出里包含Compute出来的公钥指纹。如果返回status: registered说明ReachHub和SDK之间的基本链路是通的。5.3 在analysis Agent中实现业务处理与签名验签接下来是重点analysis Agent收到invoke请求后必须验签、查权限、执行业务、签名返回。我只写核心部分# analysis_agent.py import json, time, hashlib from nacl.signing import VerifyKey class OrderAnalysisApp: def __init__(self, sk): self.sk sk self.vk sk.verify_key async def handle_invoke(self, raw_body: bytes): req json.loads(raw_body) # 1. 验签 signature bytes.fromhex(req[signature]) msg_to_verify json.dumps({ from: req[from], to: req[to], payload: req[payload], ts: req[ts] }).encode() verify_key VerifyKey(bytes.fromhex(req[pubkey])) try: verify_key.verify(msg_to_verify, signature) except Exception: return {error: signature mismatch} # 2. 此处还应查ReachHub权限表判断req[from]是否有权调用order.analyze # 省略权限校验细节生产环境必须在本地做策略缓存 # 3. 执行业务 payload req[payload] text payload.get(text, ) result self.analyze(text) # 4. 签名返回 resp { from: self.reach_id, to: req[from], result: result, ts: int(time.time()) } resp_bytes json.dumps(resp).encode() signed_resp self.sk.sign(resp_bytes) resp[signature] signed_resp.signature.hex() return resp def analyze(self, text): # 真实场景这里会调用LLM或者算法模型 # 为了跑通链路我直接做个伪分析 words len(text.split()) return { word_count: words, summary: text[:20] (... if len(text) 20 else ), sentiment: positive if 好 in text else unknown }5.4 orchestrator调用analysis并拿到结果orchestrator端的调用逻辑更纯粹# orchestrator_agent.py import asyncio, json, aiohttp async def invoke_analysis(hub_url, target, caller_key_pair): # 1. resolve获取目标endpoint async with aiohttp.ClientSession() as session: async with session.post(f{hub_url}/resolve, json{target: target}) as resp: target_info await resp.json() endpoint target_info[endpoint] # 2. 构造invoke消息并签名 import time msg { from: reach://prod/orchestrator, to: target, payload: {text: 这个订单的发货速度非常快客服态度也很好}, ts: int(time.time()) } msg_bytes json.dumps(msg).encode() # 这里需要用orchestrator自己的私钥签名 # 完整代码里ReachAgent已封装sign方法这里省略 signed caller_key_pair.sign(msg_bytes) invoke_payload { **msg, signature: signed.signature.hex(), pubkey: caller_key_pair.verify_key.encode().hex() } # 3. 把消息POST到目标endpoint async with aiohttp.ClientSession() as session: async with session.post(endpoint, jsoninvoke_payload) as resp: return await resp.json()整个过程走完后orchestrator会拿到analysis返回的分析结果。链路里最核心的机制——注册、发现、签名、验签、业务响应——全部生效。提示如果你跟我一样在本地测试注意Agent监听HTTP端口时需要一个简单的FastAPI或Flask服务来接收POST请求。不要让业务逻辑和协议SDK混在一起Controller层只负责把HTTP body交给handle_invoke结果再包装成HTTP Response返回。6. 压测与调优经验延迟、注册风暴与容错策略能跑通和能扛住生产流量是两码事。我把自己压测Agent-Reach时积累的经验都放到这里这三个问题最值得关注。6.1 消息延迟的组成与控制手段一次invoke调用的端到端延迟 resolve耗时 网络传输耗时 目标Agent业务处理耗时 签名验签耗时。其中resolve是隐藏的延迟冠军尤其是在Agent数量上升后。我的压测数据比较典型平均每次resolve请求在ReachHub上的处理时间为8ms到15ms基于SQLite和FastAPI看起来不大但如果每条消息都查在1000 QPS的调用量下就是每秒8到15秒的总耗时直接让ReachHub的CPU和数据库连接数打满。控制延迟的手段我按优先级排序调用方SDK必须内置缓存resolve结果缓存60秒以上TTL过期才重新拉取。ReachHub的查询接口做索引优化capability和reach_id都要建索引。业务Agent的消息接收端做异步化处理。不要在同一线程里又验签又查权限又调大模型验签后立刻把任务丢进消息队列马上返回“已接收”最后通过回调或者轮询拉结果。这样端到端时延虽然没变少但HTTP连接占用的时间大幅缩短并发能力成倍上去。6.2 注册风暴的防护指数退避与本地缓存Agent批量重启的时候会造成大量注册请求同时打到ReachHub这就是注册风暴。我见过最惨的一次两百个Agent同时上线ReachHub直接OOM。防护方案分两层。第一层是Agent端SDK内建随机退避比如首次注册失败后等待2秒、二次失败4秒、三次8秒最多退到30秒加上一个0到2000ms的随机抖动让注册请求在时间轴上均匀散开。第二层是ReachHub端在应用层加一个轻量限流器以namespace为维度限制每秒最大注册数超出部分直接返回“繁忙请稍后重试”。6.3 节点宕机与消息重试策略Agent节点宕机是常态Agent-Reach在路由层做的容错很直接ReachHub每30秒检查一次Agent心跳连续三次没心跳就把状态标记为offlineresolve时不再返回该endpoint。调用方的SDK在发送失败后第一步不是立即重试而是清掉本地缓存重新resolve一次因为服务可能已经迁移到新节点、换了endpoint。重试次数我建议默认三次第一次立即重试第二次等200ms第三次等1秒。如果三次都失败就别再硬试了把错误抛给上层业务做降级或者告警。无限重试是分布式系统里最典型的“自我放大故障”这个坑不要踩。7. 能避开就避开的坑跨网络穿透、证书轮换与协议兼容最后这部分是实战收尾我把三种容易让项目翻车的情况单独拿出来说你可以提前预防。7.1 Agent跨网络部署时的穿透与可达性Agent-Reach的逻辑层面再干净传输层也绕不开网络可达性的现实问题。当你有一个scheduler在A机房而worker Agent在B机房的私有子网里两者之间没有直接路由时点对点通信会失败机器配置没问题但消息就是发不出去。标准解法是给Agent-Reach加一个基于STUN/TURN的中继模块。具体逻辑是当SDK发现目标endpoint不可达时会向配置好的TURN服务发起中继请求由TURN服务器在两端的IP之间转发消息。简单说就是你得准备一台公网可达的中继机器让两侧Agent都主动连接到它在此基础上再将流量转发到目标节点。这不是Agent-Reach特有的问题任何P2P型通信框架都躲不过只是你的Agent网络如果局限在单一机房可以暂时忽略这部分。在跨机房场景下网络策略要提前和基础设施团队对齐至少放行以下端口ReachHub的8920端口Agent节点之间的9101/9102等业务端口以及TURN中继服务的认证端口。不然你本地回归测试全通过一上跨机房环境就全断排查起来会非常痛苦。7.2 证书与指纹管理的坑轮换必须留宽限期Agent长期运行后肯定会遇到密钥轮换。轮换本身不复杂关键是ReachHub和调用方之间的“指纹缓存一致性问题”。假设analysis Agent轮换了密钥ReachHub上更新了它的公钥指纹但orchestrator的本地缓存里还留着旧指纹。如果orchestrator用旧公钥去验证这个Agent的响应签名就会失败而你不知道为什么一个看似正常的调用会突然报错。解决思路是“宽限期双指纹”机制ReachHub在Agent轮换后保留旧指纹至少24小时并在resolve返回时同时返回新旧两个指纹。调用方SDK在验签时只要新旧指纹任意一个匹配就接受该响应同时把新指纹缓存下来。这个设计我跑了一年轮换过程零故障。注意轮换窗口不要太长超过48小时旧指纹就不要再保留了否则等于给攻击者留了一条旧路。7.3 不同Agent框架的协议适配别指望一套SDK吃遍天下如果你面对的Agent是LangChain或AutoGen搭建的直接用Agent-Reach SDK改写业务内部逻辑会很痛苦也会破坏原有框架的状态管理和工具调用设计。我的做法是给Agent-Reach做一层“适配器”。你只需要实现六个方法register()、resolve()、send()、invoke()、on_message()、heartbeat()然后由适配器把这些方法映射到框架的回调机制上。比如LangChain Agent里你可以把on_message回调绑定到某个Tool的_execute方法上这样原有Agent不用大改仍然可以通过Agent-Reach网络被别人发现和调用。这就把Agent-Reach定位成了一个纯通信层。它不试图接管你的业务Agent逻辑只做Agent之间的“网络连接协议”。你的Agent保持原来的样子只是在外面套了一层能被网络识别的外壳。所有涉及LLM调用的框架层都留在内部两边不冲突。我在实际项目中把FastGPT、Dify、自研Agent都接进了同一个Agent-Reach网络各自的管理后台互不影响消息却可以自由跨Agent流转。这个模式我认为才是多Agent协作最务实的形态。Agent-Reach不是在包装一个新的Agent框架而是在Agent的世界里铺设一套谁都遵守、谁都能接入的通用通信规则。到这一步距离“让所有Agent自然协作”这个目标就不远了。