
Agent 协作这件事我从单机脚本一路做到多进程、多服务最后卡在了一个最基础的问题上两个独立的 Agent 之间到底该怎么可靠地传消息。用 HTTP 轮询太笨塞 Redis 队列又太重塞进同一个进程里根本谈不上“独立”。后来我基于 hermes peer 这套点对点通信协议把整个通信层重写了一遍顺手搭了个带 Dashboard 的全栈协作系统总算把“多个 Agent 各干各的又互相配合”这件事跑顺了。这篇文章是我重构后的协议设计笔记、核心实现思路还有前端观测面板和后端调度服务联调的全过程。读完你可以照着搭出自己的一套多 Agent 点对点通信底座也能理解为什么在 Agent 协作场景里“点对点”往往比“中心化总线”更合适。1. 为什么 Agent 和 Agent 之间需要一套“点对点”通信协议1.1 单体 Agent 的天花板与多 Agent 协作的刚需早期我做 Agent 基本都是“一个超级智能体包打天下”给它装工具、塞上下文、写长篇 Prompt期待它解决所有问题。实际跑起来就会发现这条路很快触顶——上下文窗口是有限的工具链越长越容易互相干扰一个任务里的某个子步骤失败整个 Agent 的状态就被污染了。后来我转向多 Agent 架构思路很朴素把一个大任务拆成若干子任务分别交给擅长对应领域的 Agent 去处理。可拆开之后立刻遇到新问题——谁拆任务、谁派活、谁汇总结果、几个 Worker 之间要不要交流中间产物这时候“通信”就成了第一等大事。我最早用的是中心化消息队列所有 Agent 都从队列里取消息、往队列里发消息。好处是解耦坏处也很明显局部故障可能放大成全局故障队列成为单点瓶颈消息经过 Broker 中转延迟增加而且 Agent 之间如果需要高频交换中间状态走中心化队列就像两个人打电话非要经过总机转接既绕又慢。这里就是 hermes peer 这类点对点协议的用武之地。它让 Agent 之间可以直接协商通信、直接交换数据绕开中心节点。对于任务编排这类场景Manager 派活、Worker 回报结果本来就是天然的“点对点”交互模式硬塞进一个中心队列反而是架构上的过度设计。1.2 点对点架构相对中心化消息总线的取舍我整理了一张对比表是我在选型时实际参考的维度对比维度中心化消息总线点对点直接通信单点风险高Broker 挂了全链路瘫痪低节点间独立通信消息延迟多一跳中转延迟偏高直接寻址延迟更低部署复杂度需要额外维护 Broker 集群每个 Agent 内置通信模块即可消息追踪集中式日志方便排查需要额外做分布式追踪动态扩展新节点接入灵活需要服务发现机制配合适合场景大量解耦事件、跨团队集成有明确上下游的 Agent 协作多 Agent 协作里也有大量“你做完一步通知我继续”的编排型消息天然符合“点对点”模式。hermes peer 的设计就是将这种交互关系显式化消息只从 A 到 B不经过第三方存储。相比之下中心化总线更适合事件流驱动的场景比如某个 Agent 产生了日志广播给所有订阅者。这两种架构不是替代关系而是互补关系只是在 Agent 两两协作这个具体问题上我更倾向点对点。1.3 hermes peer 的设计目标信使而不是国王hermes peer 这套协议给我的第一印象是克制。它不做任务编排不做 Agent 生命周期管理不做消息持久化几乎所有“平台级”的功能它都刻意回避了。它只专注做一件事把 Agent A 的一条消息可靠、有序、低延迟地送到 Agent B。这个定位很接近希腊神话里赫尔墨斯的角色——跑腿的使者不是发号施令的国王。之所以强调这点是因为我看过太多通信框架死于“什么都要管”。一旦协议层开始管任务调度、管状态持久化它就会无限膨胀最后要么没人用要么更难演进。hermes peer 把通信层做薄了反而让上层的 Agent 编排、任务调度、状态管理都可以独立演进互不绑架。2. hermes peer 的协议解剖信封、寻址、保序与背压2.1 消息信封用固定帧头换来的确定性先看协议最底层的部分——消息在网络上到底长什么样。hermes peer 的核心是一条“信封 载荷”的消息结构信封里装着路由信息和元数据载荷才是真正的业务数据。这个设计很像寄快递包裹外面贴着寄件人、收件人、快递单号里面才是真正的货。快递员不需要拆开包裹就能分拣路由协议层也不需要反序列化业务对象就能完成转发。我基于这个思想做了自己的 TypeScript 实现信封的大致结构如下interface HermesEnvelope { protocolVersion: number; // 协议版本号用于兼容协商 messageId: string; // 全局唯一消息 ID type: request | response | event; from: string; // 源 Agent ID to: string; // 目标 Agent ID timestamp: number; // 发送时间戳毫秒 replyTo?: string; // 回信消息 ID用于请求/响应配对 idempotencyKey?: string; // 幂等键用于消费端去重 ttl?: number; // 存活时间过期消息直接丢弃 payload: unknown; // 业务数据 }帧传输阶段我采用的是“固定头部 变长载荷”的编码方式。头部固定 16 字节前 4 字节放魔数0x48 0x50 0x54 0x50即“HPTP”接下来 1 字节放协议版本4 字节放载荷长度再往后是消息类型和标志位。这样设计的好处是解析器只需要读固定长度的头部就能知道载荷多大、需不需要继续读取不用反复扫描流数据。实际项目里我直接用了 WebSocket 作为传输层省去自己处理 TCP 粘包拆包的麻烦帧头设计则保留给底层切换为自定义 TCP/UDP 传输时使用。2.2 Agent 寻址与动态注册Registry TTL 租约点对点通信要对谁说话首先得知道对方在哪。hermes peer 的寻址模型里没有复杂的分布式哈希表也没有 DNS 级别的抽象它用的是一套轻量级的 Registry 机制每个 Agent 启动时向 Registry 注册自己的 ID、地址如hermes://host:port、能力标签比如worker:翻译、worker:代码评审Registry 会返回一个带 TTL 的租约Agent 需要周期性续约如果 Agent 宕机或者网络分区超过 TTL 后 Registry 会自动把这条记录标记为不可用。我踩过的坑在续约间隔上。最开始我把 TTL 设为 30 秒续约周期设为 5 秒看起来挺保守。但当 Agent 数量到了几十个每个 5 秒续约一次Registry 的写入压力就上来了而且网络抖动时一次续约失败就会导致 Agent 被错误摘除。后来我把 TTL 放宽到 60 秒续约周期 15 秒并在续约请求里加上“本 Agent 当前处理的活跃会话数”Registry 依据这个负载信息来决定是否通知调用方“这个 Agent 正忙”。这在多 Agent 调度里特别有用——任务分配前先看负载而不是总是把新任务塞给闲置的 Agent。2.3 消息确认、重试与幂等键网络不可靠协议要可靠点对点直连虽然少了中转节点网络依然是不可靠的。hermes peer 协议里对可靠性的回答很务实确认 重试 幂等。发送方发出消息后接收方处理完毕会回一条ack消息。发送方如果在超时窗口内没收到ack会按指数退避策略重试第一次 1 秒后重试第二次 2 秒第三次 4 秒最高不超过 30 秒最多重试 5 次。这个策略跟 TCP 的重传退避思路类似但发生在上层业务协议里好处是可以携带业务语义——如果一个 Worker 正在跑一个耗时的代码审查它可以在真正处理完之前先回一条pending消息告诉发送方“我收到了但还没干完别急着重试”。这里最容易踩的坑是重复消息。假设发送方重试了 3 次接收方第一次已经处理成功了只是ack回包丢了那第 2、3 次重试的消息就会被重复处理。所以协议里强制要求每条消息携带idempotencyKey。我实现时的做法是接收方维护一个固定大小的 LRU 缓存专门存放最近处理过的幂等键新消息进来先查缓存命中就直接返回上次的处理结果不再执行业务逻辑。这个设计在“任务分发 结果回收”场景里特别关键否则一个重复分发就可能让两个 Worker 抢着处理同一件事。表格式对比一下我实际用过的三种可靠性策略策略实现成本延迟适用场景发后即忘fire-and-forget极低最低日志、指标上报确认 重试中中任务分发、命令下发持久化 事务消息高较高订单、支付等强一致场景Agent 协作里通常用第二种就够了。第三种虽然安全系数最高但跟 hermes peer 刻意做薄的理念相悖除非业务场景强制要求否则我不会引入。2.4 背压信号别让你产的消息压垮消费方点对点通信里最容易忽视的是背压backpressure。中心化队列可以通过消费者积压指标来观察点对点直连就没有这个天然的缓冲区域了。如果发送方是某个高速产出任务拆解的 Manager接收方是处理速度较慢的 Worker消息会直接在 Worker 端的接收队列里积压内存飙升甚至 OOM。hermes peer 在设计里把背压问题提到了协议层面接收方通过独立的控制通道定期向发送方上报自己的“处理水位”包括当前队列积压数、平均处理耗时、最近一分钟的丢弃数。发送方拿到这些指标后如果检测到接收方水位过高可以采用以下策略降低发送速率限流让 Worker 追上处理速度把后续消息改发给其他同能力的 Worker这要求 Manager 知道每个 Worker 的能力标签直接熔断直到 Worker 恢复并发出低水位信号。我在初版实现里根本没有背压机制后果是 Manager 一次性把 50 个子任务全塞给一个 WorkerWorker 队列爆掉任务静默丢失。加入水位上报之后Manager 会动态调整发送节奏整个系统的吞吐反而上去了。这个经验让我深刻体会到在 Agent 协作系统里“飞快地发消息”不等于“高效地完成任务”消息发再快接收方消化不了就是灾难。2.5 心跳与超时如何识别“死掉”的 AgentAgent 崩溃、网络分区、进程被 OOM Killer 杀掉……这些都是多 Agent 系统的日常。hermes peer 用轻量级心跳来探活每个 Agent 每隔一个固定周期我配置的是 5 秒向它“关注”的对端发送心跳帧对端在超时阈值我配置的是 15 秒内没收到心跳就把这条链路标记为“可疑”再过一个阈值周期仍没恢复就正式宣告对方失联并通知上层编排器重新调度该 Agent 正在处理的任务。这个探活机制比 Registry 里的 TTL 续约更精细。Registry 的 TTL 解决的是“这个 Agent 还活着吗”心跳解决的是“我跟这个 Agent 的通路还通吗”。两者并行使用才能处理复杂的网络分区场景——Agent 进程其实还活着但它所在的主机和我看不到对方了这时候如果只依赖 Registry 的续约可能等不到租约过期消息已经丢失很久了。3. 全栈协作实战一个带着管理面板的多 Agent 任务流3.1 整体架构TypeScript 客户端 Node 服务 Vue 观测面板讲完协议层来说说我是怎么把它落成一个全栈项目。我搭的系统叫它“Agent 协作工坊”核心是一个任务编排场景一个 Manager Agent 负责剖析需求、拆分子任务一组 Worker Agent 各自领取任务执行完成后回传结果前端面板实时展示每一步的消息流转。技术栈用的是我一直很顺手的组合Node.js TypeScript 作为后端运行时和协议实现语言前端用 Vue ECharts 做消息流观测面板通信层基于 hermes peer 协议自己封装的类型安全客户端。为什么选 TypeScript 而不用 Python因为我要做的是全栈协作前后端共享同一套类型定义可以省掉大量接口联调成本。AgentProfile、TaskPayload、HermesEnvelope这些核心类型都放在一个共享包里前端能直接感知到协议层的类型结构Manager 和 Worker 的代码也不会因为“字段名不一致”吵起来。我实际是结合了自己一直用的 Claude Code OpenSpec Superpowers 三件套来开发这套系统的OpenSpec 负责把需求写成明确的技术规格Superpowers 提供开发流程模板Claude Code 负责生成大部分胶水代码。我自己的精力集中在协议细节和系统设计上。这次经验让我更加确定在 AI 辅助编码的时代一个人从协议层写到前端面板并稳定交付是完全可行的——但前提是协议这种“地基”必须自己真正想清楚。3.2 AgentClient封装 hermes peer 协议的核心类所有 Agent 都继承自同一个AgentClient基类这样协议层的细节被完全隔离上层 Agent 只需要关心业务逻辑。下面是我精简后的核心实现export abstract class AgentClient { protected transport: HermesTransport; protected registry: RegistryClient; protected pendingRequests new Mapstring, { resolve: Function; reject: Function }(); protected idempotencyCache new LRUCachestring, unknown({ max: 2000 }); async sendRequestT(to: string, method: string, payload: unknown, options?: RequestOptions): PromiseT { const messageId randomUUID(); const envelope this.buildEnvelope({ messageId, type: request, to, payload: { method, args: payload }, idempotencyKey: options?.idempotencyKey ?? messageId, ttl: options?.ttl ?? 30_000, }); return new PromiseT((resolve, reject) { this.pendingRequests.set(messageId, { resolve, reject }); setTimeout(() { if (this.pendingRequests.has(messageId)) { this.pendingRequests.delete(messageId); reject(new Error(Request ${messageId} timed out)); } }, envelope.ttl); this.transport.send(envelope); }); } protected async handleEnvelope(env: HermesEnvelope): Promisevoid { // 幂等键查重 if (env.idempotencyKey this.idempotencyCache.has(env.idempotencyKey)) { const result this.idempotencyCache.get(env.idempotencyKey); this.transport.send(this.buildAck(env, result)); return; } if (env.type request) { const result await this.handleMethod(env.payload.method, env.payload.args); this.idempotencyCache.set(env.idempotencyKey, result); this.transport.send(this.buildResponse(env, result)); } else if (env.type response) { const pending this.pendingRequests.get(env.replyTo); if (pending) { this.pendingRequests.delete(env.replyTo); pending.resolve(env.payload); } } } protected abstract handleMethod(method: string, args: unknown): Promiseunknown; }这段代码里有几个细节值得展开说说。pendingRequests是请求/响应配对的映射表消息发出去后把resolve和reject存起来等响应回来时按replyTo取出来执行。超时用setTimeout实现超时后直接reject并把条目从表里删掉避免内存泄漏。超时时间为什么取 30 秒我统计过系统里任务的实际耗时中位数在 3 到 8 秒最慢的代码评审任务平均 20 秒。取 30 秒意味着绝大多数业务能正常完成又不会让调用方无限等待。如果你跑的是几十秒甚至几分钟的长任务建议把超时做成可配置或者像前面说的那样引入pending消息来延长等待时间。3.3 Manager Agent把大需求拆成可并行的子任务Manager Agent 是系统的中枢。用户提交需求后Manager 先做需求分析把大任务拆解成多个原子子任务再根据 Registry 里 Worker 的能力标签和负载信息做分配。这个“拆解—分配—汇总”的过程本身就是典型的点对点通信协作。简化版的 Manager 核心逻辑如下async orchestrate(task: string): Promisevoid { const subtasks await this.planSubtasks(task); // 调用 LLM 拆解任务 const workers await this.registry.query({ capability: code, status: healthy }); const assignments this.balance(subtasks, workers); const results await Promise.allSettled( assignments.map(({ agentId, subtask }) this.client.sendRequest(agentId, execute, { subtask, context: this.sessionContext, idempotencyBase: this.sessionId, }) ) ); // 汇总成功结果失败结果标记后重新分配 this.collectAndReport(results); }planSubtasks是唯一用到 LLM 的地方后续的任务派发、结果回收、失败重分配全都是纯工程问题。这其实是我设计上的一个核心决策不要让 Agent 的“智能”掩盖掉通信层的短板。之前我天真地认为只要模型足够聪明网络乱一点也能自己补回来这只是自我安慰协议层不可靠上层模型再聪明也只能收拾烂摊子而且非常消耗上下文 token。失败重分配是我在全栈联调中发现必须补上的逻辑。某个 Worker 在处理子任务时进程崩溃Manager 侧Promise.allSettled会捕获失败这时 Manager 需要把这个子任务重新分配给另一个同能力 Worker。但是要留意如果子任务本身是幂等的还好如果它不是幂等而重分配后执行了两次就会产生重复副作用。所以我要求所有子任务都显式声明自己的幂等性非幂等任务重分配前必须有补偿动作比如回滚部分操作。3.4 Worker Agent消费、执行、回报Worker 侧的实际实现比 Manager 简单直接。它向 Registry 注册自己的能力标签然后进入事件循环监听来自 Manager 的请求。核心代码片段如下export class CodeWorker extends AgentClient { constructor() { super(worker-code-01, [code, refactor, review]); } protected async handleMethod(method: string, args: any): Promiseunknown { switch (method) { case execute: return this.executeTask(args); case ping: return { pong: Date.now() }; default: throw new Error(Unknown method: ${method}); } } private async executeTask(args: any): PromiseTaskResult { const { subtask, context, idempotencyBase } args; const traceId randomUUID(); // 这里会调用代码生成工具、执行命令、收集反馈 const patches await this.generateAndApply(subtask, context); const testsPassed await this.runTests(subtask, patches); return { subtaskId: subtask.id, traceId, patches, testsPassed }; } }Worker 端最要注意的是不能让一个长任务卡死整个事件循环。因为 Node.js 单线程如果executeTask内部是同步的耗时代码其他消息包括心跳就无法被处理导致对端误判这个 Agent 失联。我这里用的方案是把真正的代码生成和测试执行放到子进程里主进程只负责调度和消息收发。子进程跑完通过回调把结果塞回消息队列。这个设计带来一个额外好处即使某个子任务让子进程崩溃了主进程的通信链路依然健在Manager 能收到“这个 Worker 挂了但通信模块还活着”的状态然后决定是重启一个 Worker 实例还是把任务转移给别的节点。比起整个进程一起挂掉这种“部分失败”的模型更符合真实的生产环境。3.5 观测面板把点对点消息流变成可视化协议是给机器看的但排查问题的时候人必须能看明白。我给系统加了一个实时观测面板这也是“全栈”里前端那部分。面板的功能有三块节点列表展示所有在线 Agent 的 ID、能力标签、当前负载、心跳状态消息链路以时间轴形式展示每条消息从 Manager 发出到 Worker 回 ack 的完整链路告警面板展示重试次数、超时消息、幂等命中次数、积压队列长度等 Key Metrics。消息链路这一块最有用。每次请求发出前端通过 WebSocket 订阅消息事件流还带上messageId、from、to、duration。某个任务卡住时我能一眼看出来是 Manager 那边没发出还是 Worker 处理太慢还是回包在网络里丢了。前端我用 Vue 3 加 ECharts 做了一张力导向图Agent 是节点、消息是连线消息频率高的节点之间连线更粗系统是否出现“热点”一目了然。这套观测面板让我联调时省了太多时间。之前没有面板的时候排查“某个任务为什么没执行”得去翻多个 Agent 的日志现在直接看时间轴就能定位。说实话协议好写系统难查可观测性才是多 Agent 系统真正烧时间的地方。4. 跑通案例后必须面对的四个坑4.1 坑一Agent ID 冲突与重复注册系统刚跑起来没几天我发现任务经常被错误执行排查了半天结果是两个 Worker 进程用的同一个 Agent IDworker-code-01。原因是我本地起了两个终端每个终端都跑了一遍npm run dev:worker两个进程都向 Registry 注册了相同 ID。Manager 按 ID 寻址时消息只发到其中一个而另一个进程偶尔也在干活两头状态完全不一致。之后我在 Agent 基类初始化那里加了注册前检查向 Registry 发起checkAgentId请求如果注册表里已存在相同 ID 且健康状态为 active直接拒绝启动并给出清晰报错。我还给每个 Agent 加了一个启动时间戳注册时写入instanceId字段。就算 ID 相同instanceId也能区分出两次不同的启动排查日志时能精确到具体进程。4.2 坑二消息乱序——多路复用下的顺序问题另一个隐蔽的问题是消息乱序。某次 Manager 连续给同一个 Worker 发了三条子任务要求按顺序执行。结果因为 1 号子任务处理时间较长3 号反而先完成并回传了结果Manager 拿着不完整的结果做汇总把输出全搞乱了。要理解这个问题得明白消息队列和点对点直连的本质区别消息队列有 FIFO 语义点对点直连默认是异步多路复用的没有顺序保证。hermes peer 自身不提供跨消息的全局顺序因为它认为顺序是业务问题——有的任务需要强顺序有的完全不用。我的解决方案是在协议之上增加一个可选的sequenceNumber字段。Manager 给同一个 Worker 发消息时在信封里带上自增序号Worker 端按(from, sequenceNumber)排序处理如果发现有跳号比如收到了 1 和 3没收到 2就把 3 先放进等待缓冲区超时还没等到 2 就上报 Manager 补发。这个“滑动窗口 超时补发”的方案实现成本不高却解决了大多数需要强顺序的任务场景。4.3 坑三重试风暴与幂等处理我在前面讲了幂等键的设计但这个机制第一次上线时就翻车了。当时 Manager 给 Worker 发任务后由于 Worker 处理时长超过了超时阈值Manager 开始重试重试消息带有相同的idempotencyKey。Worker 端第一次执行成功并回传了结果但ack消息堵在发送队列里还没发出去这时候第二条重试请求已经来了。Worker 的幂等缓存本该命中但我犯了个低级错误幂等缓存只存了“已成功”的结果没存“执行中”的中间状态所以第二条重试消息没命中缓存又从零开始跑了一遍导致副作用重复。修复方案是给幂等缓存增加“执行中”状态enum IdempotencyStatus { RUNNING running, COMPLETED completed, FAILED failed, } interface IdempotencyEntryT { status: IdempotencyStatus; result?: T; updatedAt: number; }Worker 收到请求后先把状态置为RUNNING再把幂等键写入缓存处理完成后再把状态更新为COMPLETED并写入结果。并发重试请求看到RUNNING时不会重复执行而是挂起等待第一个请求完成。这个小小的状态机解决了重试风暴下的重复执行和并发竞争两个问题。4.4 坑四全栈联调时的协议版本兼容前后端联调阶段我遇到一个很典型的版本兼容问题。前端页面和 Manager 跑在同一台机器上用的是最新的协议帧格式但 Worker 部署在另一台开发机上代码没更新还是旧格式。Manager 按新格式编码帧头Worker 按旧格式解析直接失败。hermes peer 协议在帧头里设计了protocolVersion字段但实际开发中这个版本号形同虚设——我不会在每次改动协议结构时都去递增它。直到踩了这个坑我才意识到既然字段存在就必须被认真使用。我制定了一条规则任何协议结构变更必须递增版本号并在握手时带上双方支持的版本范围如果发现对端不支持当前版本高版本一方选择降级到对方支持的版本或者直接断开并给出升级提示。全栈联调和纯后端联调不一样的地方在于前端页面一旦发布弱网环境下的用户可能不会立刻刷新到最新代码导致版本长时间不齐。所以协议栈必须往前兼容至少一个大版本这跟 Web 里老浏览器访问新网站是同一个逻辑。5. 调优与扩展思路从能用到好用5.1 消息合并与减少握手开销点对点通信的消息数一旦上去握手开销和序列化开销就变得不可忽视。我实际压测过一个小场景Manager 给 Worker 发 1000 条小 JSON 消息每条消息走一次完整编解码花费的 CPU 时间是“把这些消息合并成 10 个大批次消息”的 5 倍以上。当 Agent 数量增多、协作频率变高时这个差异会被放大。我做的优化是引入批处理允许在一个信封的payload里放一个消息数组接收方收到后按数组顺序逐条处理。批处理和幂等键配合使用时要注意一个批里的某条消息若是重复的不能把整个批都当重复丢掉。所以我的批处理编码里每条消息都保留独立的messageId和idempotencyKey只有逐条命中的才会被跳过。5.2 主题广播与路由扩展hermes peer 的基础模型是一对一的点对点但 Agent 协作里还有“一对多”的场景Manager 状态变更了想通知所有 Worker某个 Worker 产出了公共情报想广播给所有协作方。我在协议层之上加了一个轻量级“主题”机制Agent 可以向 Registry 声明自己订阅了某个主题发往该主题的消息会被 Registry 转发给所有订阅者。这个机制本质上是对点对点能力的一个补充不改变核心帧结构只是在信封的type字段里增加一种eventto字段填写主题名而非具体的 Agent ID。我用它实现了“会话广播”比如某个会话被用户取消了Manager 发一条event: session.canceled所有相关 Worker 收到事件后停止当前工作这对于省 token 和释放算力很有帮助。5.3 链路追踪与消息轨迹多 Agent 系统排查问题最大的痛点是调用链不清晰。一个任务的完整链路是Manager 拆解 → 分发 Worker A → Worker A 调 Worker B 获取中间数据 → Worker A 汇总 → 回传 Manager。单靠日志里的关键词搜索太痛苦了。我参考了分布式 trace 的思路在信封里增加了traceId字段整个链路的所有消息共享同一个traceId每跳转发时附带parentSpanId和spanId。观测面板收到这些数据后能画出一棵完整的调用树准确还原每个环节的耗时和结果。这套东西看起来是“额外工作”但一旦系统里的 Agent 超过 5 个没 trace 几乎无法排查跨节点问题。5.4 边界之外鉴权与加密最后说说我目前没有实现的边界。hermes peer 协议本身不带内建的鉴权和加密能力这在可信内网环境里够用但只要是跨网络、跨集群互信的环境就必须补上。我的建议是直接在传输层解决WebSocket 升级时使用 TLS在连接建立时通过签名秘钥做双向认证消息体可以用 AES-GCM 加密后再放入payload。这样协议本身保持简单安全能力通过传输层和配置去加强而不是塞进协议核心。一点最后的体会把 hermes peer 这套点对点通信协议从了解、实践到全栈落地走了一遍之后我对 Agent 协作系统的理解变化很大。其实多数系统在一开始的规模下什么样子的通信方案都能跑瓶颈都在超出临界点之后才爆发任务量上来了消息就丢Agent 数量上来了寻址就乱链路复杂了问题就找不到根因。与其等出了问题再打补丁不如在设计阶段就把通信协议、寻址机制、幂等、重试、背压、可观测性这些要素一次想清楚。我给自己的原则是上层可以花哨底层必须扎实。Agent 的智能负责解决“做什么”协议层负责确保“靠得住”两者缺一不可。这篇笔记里的实现和踩坑如果你正准备搭自己的多 Agent 系统可以直接拿去对照检查能少走不少弯路。