Agent点对点通信协议设计与断线恢复全栈实践 📅 发布时间:2026/9/12 8:54:09 👁 浏览次数: 1. 从三次断连事故说起Agent间通信为什么需要新协议上个月排查一个Agent协作系统的问题日志里反复出现stream disconnected before completion: io error: peer closed connection with...紧接着是curl: (35) recv failure: connection reset by peer再往后java.io.IOException: connection reset by peer。三个不同的报错指向同一个现象两个Agent进程之间的长连接在传输中途被对端强硬掐断而且掐断前后没有任何应用层日志。这种问题在传统Web服务里并不难处理浏览器请求断了重发一次就行。但Agent和Agent之间的点对点通信完全不同——它们不是一次请求换一次响应而是需要在一条长连接上持续交换带状态的消息包括任务分发、进度上报、工具调用结果、流式推理输出。每次断连都意味着当前上下文可能丢失轻则重跑一遍任务重则两个Agent各执一词整个状态机直接错乱。我后来把整套通信模型推倒重做设计了hermes peer这套面向Agent的点对点通信协议。这篇文章会把我在协议设计、全栈协作、部署落地和故障排查上的完整经验拆开来讲不绕弯子直接说清楚hermes peer在每个环节到底做了什么、为什么这么做、以及真实环境里会遇到哪些坑。适合正在做Agent开发、分布式任务编排或者被各种peer断连问题折磨过的后端工程师。1.1 事故复盘peer closed connection 到底是谁关的先还原一下那三次断连事故的共同特征。peer closed connection是一条非常底层的报错它只说明一件事TCP连接的对端主动发了FIN或RST包。但主动关闭的原因可以差出十万八千里。第一次事故发生在两个Agent跨机房通信时中间经过了一层负载均衡设备。Agent A向Agent B发送一个较大的流式响应写入到一半连接被中间设备静默回收。这种情况最隐蔽因为两端进程都还活着谁也没主动关连接但网络路径上某个节点认为空闲超时到了直接清掉了会话表。TCP层的心跳根本救不了因为TCP keepalive默认间隔是2小时而且它只探测网络通不通不探测对方进程还有没有能力处理业务消息。第二次是典型的协议层问题。客户端按自定义二进制格式解析服务端返回但服务端升级后改了字段顺序客户端解析出明显不合法的长度字段随即关闭连接。日志里表现为error: protocol fault (couldnt read status)随后是connection reset。这种问题本质上是通信双方对协议的理解不一致再多的重连也无济于事。第三次最经典Agent B在处理一个长时间任务时主线程被阻塞无暇读取socket缓冲区。Agent A持续发送数据内核缓冲区写满后A收到对端窗口为0的提示等待一段时间后超时主动reset。从B的视角看它什么都没做连接就断了从A的视角看B已经死了。三次事故让我意识到Agent间通信不能靠某个单一层面的机制解决所有问题。TCP负责可靠传输HTTP负责应用语义但Agent之间需要的是一套更贴近业务场景的协议既要能感知对端是否活着又要能表达这条消息是任务请求还是进度回调还要能在断线后恢复上下文。这就是hermes peer的起点。1.2 中心化消息队列不香吗为什么Agent间需要点对点可能有人会问现有消息队列MQ不是已经很成熟了吗Agent之间的消息丢进Kafka或者RabbitMQ按topic消费不就不用管连接断不断了吗MQ当然有它的适用场景但放在Agent点对点通信里存在三个结构性痛点。第一是延迟。Agent之间经常需要毫秒级的响应尤其是当一个Agent在另一个Agent的推理过程中边计算边返回token时。中心化broker至少引入一次额外的网络跳转而且在高吞吐下broker本身的处理队列会放大延迟抖动。第二是状态割裂。MQ天然是生产者-消费者模型消息一旦被消费broker就认为任务完成。但Agent之间的协作不是一次性消息消费而是双向的、多轮的对话。Agent A给Agent B发了任务B在执行过程中需要流式回传中间结果甚至反过来向A请求更多信息。这种双向语义用MQ实现非常别扭通常要同时建两个queue并且手动维护关联ID。第三是单点依赖。引入broker意味着整条通信链路多了一个必须高可用的组件。如果broker挂掉所有Agent全部瘫痪。而点对点通信虽然没有broker那样的中心枢纽但如果消息没有掉地可靠性就得靠通信双方自己保障。hermes peer的设计目标很明确在保留点对点长连接低延迟、双向通信优势的同时补齐可靠性、协议一致性、断线恢复这些能力。它不是一个替代MQ的方案而是专门服务于Agent之间高频率、带状态、需要流式交互的场景。简单说MQ适合事件分发hermes peer适合两个Agent在合作完成一件事。2. 协议骨架拆解握手、帧格式、心跳与断线恢复这一节是整篇的核心。很多所谓的Agent通信框架只是在HTTP之上封装了一层JSON遇到connection reset by peer时除了重试毫无办法。hermes peer不一样它在设计协议的第一天就把对端可能随时消失作为默认前提。2.1 握手阶段不只是验证身份更是能力协商握手的第一个动作是身份认证。每个Agent节点在启动时都会生成一对密钥并用它签名自己的节点ID。两个Agent建立连接时双方交换节点证书验证对方是否在自己的信任列表里。这一步必须走证书级别的验证而不是简单地比对token——因为Agent间通信链路中可能经过其他服务token容易被截获证书配合签名可以保证每条消息都来自合法节点。握手阶段做完认证后还有一个容易被忽略的环节能力协商。通信双方会交换自己支持的消息类型、最大帧尺寸、心跳周期、是否支持流式响应等参数。这非常重要。我见过太多系统服务端升级了能力客户端还是老逻辑双方解析方式不一致最终表现为protocol fault。协商完成后两端就知道对方的底线不会发对端不认识的帧类型。典型握手流程大致是这样客户端发送Hello帧携带节点ID、公钥指纹、支持的协议版本号。服务端验证客户端身份返回Welcome帧包含服务端能力列表和会话ID。客户端发送Ack帧确认能力列表双方进入Established状态。如果双方对协议版本号有分叉任何一方都可以在业务开始前拒绝连接。从握手起就明确我们接下来要谈什么很多后续的协议错乱问题就能在入口处被拦截。2.2 消息帧设计stream_id 是灵魂握手完成之后所有业务数据都封装在统一的消息帧里。帧头是定长的包含以下关键字段字段长度含义magic4字节固定魔数用于快速识别非法连接version1字节协议版本号协商失败时据此报错type1字节帧类型如DATA、ACK、PING、PONG、CLOSEflags1字节控制位如FIN、SYN、RSTstream_id4字节流ID用于多路复用和流式会话sequence4字节序列号保证有序性和去重length4字节载荷长度载荷部分放业务数据比如Agent任务描述、工具调用参数、流式token。单独提取metadata字段则用于放trace_id、span_id、超时控制等。这里最值得展开的是stream_id。它允许一条物理连接上同时跑多条逻辑流。比如Agent A同时向Agent B发三个任务两个是普通请求一个是流式生成三条流通过不同的stream_id区分互不阻塞。这个设计参考了HTTP/2的多路复用思路但比HTTP/2更贴合Agent场景——每条流都可以有独立的超时、独立的重试状态一条流出错不会影响其他流。为什么不用HTTP/2直接当传输层因为HTTP/2的流语义是单向的服务端无法主动向客户端发起一条新流。Agent通信里经常出现Agent B正在执行任务中途需要主动向Agent A索取更多参数。如果基于HTTP/2B只能等A的下一个请求或者再开一条新连接。hermes peer让任一方向都可以发起新流所以它是真正的全双工点对点通信。2.3 心跳与断线检测感知对端存活的正确姿势前面提到TCP keepalive不够用。hermes peer在应用层设计了自己的心跳机制核心是定向PING/PONG。节点B每N秒向节点A发送一个PING帧帧里携带一个单调递增的序号。节点A收到后必须立刻回一个携带相同序号的PONG帧。如果B连续M次没收到对应序号的PONGB就可以判定A处于不健康状态主动关闭连接并进入重连流程。心跳周期不能设得太激进。我之前犯过的错是图省事把N设为1秒M设为3结果机房一次网络抖动导致几十个Agent互相误判全部进入重连风暴。后来调整为N5秒M5兼顾灵敏度和稳定性。具体数值要看业务容忍度但一个基本法则是心跳误判的代价远高于心跳迟钝的代价。因为误判会引发大规模重连而迟钝最多让你多等几秒才发现对端失联。更关键的是心跳不是只发空包。我会在PING帧里携带当前节点的基础负载信息比如待处理队列长度。这样对端不仅能判断你是不是还活着还能判断你有没有能力继续处理消息。当对端负载过高时发送方可以提前降速避免消息堆积导致缓冲区溢出最终触发connection reset。2.4 断线恢复让连接重置不再是事故断线不可怕可怕的是断线后两端状态对不上。hermes peer的断线恢复分三层第一层是快速重连。检测到对端失联后节点以指数退避间隔1s、2s、4s…上限30s发起重连中间穿插随机抖动避免多个节点同时重连。第二层是会话恢复。重连成功后双方通过握手阶段协商出的会话ID把断线期间的消息队列同步一遍。发送方会把未确认的消息重新发送接收方根据sequence字段去重。这里的前提是消息必须持久化到本地否则进程重启后什么都没了。第三层是流式续传。对于正在传输的流如果断线前已经发到第100个token恢复后不需要重发前100个双方保存了各自的消费位点直接从断点续传。这要求发送方不能把已发送但未确认的数据轻易丢弃要保留一段时间。有了这三层stream disconnected before completion就不再是一个致命错误而是一个触发恢复流程的信号。需要注意的是恢复不等于百分百无缝业务层还是要做好收到重复消息的幂等准备。3. 全栈协作案例规划Agent与执行Agent的点对点协作理论说再多不如看一个完整的全栈案例。我挑一个最常见的业务闭环用户下达一个复杂任务规划Agent拆解步骤执行Agent负责调用工具并返回结果。整个过程涉及多轮双向通信、流式输出、异常中断恢复正好能压测hermes peer的各个能力。3.1 场景设定两个Agent一条长连接假设系统里有两类Agent节点规划AgentPlanner负责任务分解生成步骤列表对执行结果做校验和汇总。执行AgentWorker接收单个步骤调用外部工具返回执行结果或流式中间状态。它们之间不通过中心服务转发而是直接建立hermes peer点对点连接。这样规划Agent可以实时感知执行Agent的执行状态执行Agent也可以反向向规划Agent请求更细颗粒度的指令。这个场景典型在什么地方它包含了三种通信模式普通请求/响应、流式中间进度、以及执行Agent反客为主的反向请求。三种模式如果分开用不同的组件实现代码会非常割裂在hermes peer里它们只是不同stream_id上的数据流。3.2 通信时序一次任务从开始到完成整个协作过程的通信时序大致如下规划Agent收到用户任务后分析出需要执行三个子步骤。它通过hermes peer向执行Agent发起一条新流发送TaskEnvelope消息包含步骤列表、上下文引用、整体超时时间。执行Agent确认接收回一个AckMessage。此时流的session正式建立。执行Agent开始执行第一个步骤每完成一个阶段就给规划Agent发送ProgressEventpayload里包含当前执行状态、已消耗的token数、中间输出。规划Agent可以据此给前端推送实时进度。执行过程中执行Agent发现自己缺少一个关键参数于是主动向规划Agent发起一条新流发送RequestInfo消息。这条新流与之前的任务流是不同的stream_id但它们共享同一条物理连接。规划Agent查询到参数后在反向流上回传参数。执行Agent收到后继续工作。所有步骤执行完毕执行Agent通过原任务流发送ResultMessage携带最终结果。规划Agent校验无误后关闭该流。你发现没有整个过程里始终只有一条物理连接。双向的数据交换通过不同的流交错进行不会互相等待。这就是全双工点对点通信的价值。如果用HTTP轮询步骤4根本无法自然发生你只能额外建一个参数回调接口多出一堆胶水代码。3.3 流式输出遇上断线的处理真实场景里执行Agent如果接的是大模型服务返回结果往往是流式的。执行Agent会把模型吐出的token逐个封装成DATA帧通过hermes peer实时推给规划Agent。规划Agent这边可能还在等待最终结果就已经能收到中间token并转给前端。stream disconnected before completion最容易发生在这个阶段。一旦执行Agent与模型服务之间的链路抖动执行Agent传给规划Agent的流也会中断。没有断线恢复机制时前端会看到一个半截的生成结果用户只能重新发起任务。在hermes peer里执行Agent在发送每个DATA帧时会带上该帧在整条流内的offset。规划Agent收到断线信号后会记录最后一次完整收到的offset。恢复连接后规划Agent向执行Agent发起一个ResumeStream请求带上自己的消费offset执行Agent从该offset继续发送。这样用户看到的生成过程几乎是无缝续接的。还要注意流式场景下心跳机制要更敏感。模型输出一次可能持续几十秒如果中间没有数据帧单纯的TCP层很难判断是模型还在算还是连接已经死了。hermes peer在流上设置了独立的stream heartbeat即使没有业务数据流也会定期发一个控制帧让接收方知道这条流还活着。3.4 可观测性全链路沟通的最后一环协议层做得再好如果出了问题看不见运营成本会很高。hermes peer在每个消息帧的metadata里都带了trace_id和span_id一条任务从规划Agent到执行Agent再到模型服务会串起一条完整的链路。控制台页面一般由hermes studio或webui加载展示的不再是某条HTTP请求的耗时而是Agent之间每条流的生命周期什么时候建流、发了多少帧、有没有重传、断了多少次、恢复耗时多少。排查问题的时候先看是不是链路中有节点断连再看断连点是哪一层比翻原始日志高效太多。这里分享一个排查经验如果看到一条流的retransmission count很高但最终没有断线大概率是网络质量不佳。不要急着改协议先检查节点之间的物理链路。如果stream disconnected出现在没有业务消息的空闲期优先怀疑是中间设备空闲超时这时需要把心跳周期调小。4. 部署落地连接本地模型与错误排查实战协议设计得再精巧最终还是要落到部署上。很多人在本地尝试跑Agent协作系统时第一步就卡在怎么让我的Agent和另一个Agent连上。这一节我讲讲最常用的部署方式以及那些折磨人的连接错误到底该怎么查。4.1 hermes agent 安装与连接本地模型以最常见的个人开发环境为例通常会在一台机器上同时跑两个Agent进程并通过hermes peer互相通信。这种情况下不需要中心网络两个进程直接通过localhost建立长连接。基本步骤可以归纳为安装hermes agent运行时启动后会在指定目录生成一份节点配置包含节点ID、密钥对和监听端口。配置信任列表把另一个节点的公钥指纹加进去。这一步是为了保证两个Agent在握手阶段能互相认账。配置模型服务端点。hermes peer本身不关心你用的是哪个模型它负责把Agent之间的消息送达但Agent内部的推理需要模型服务来执行。启动两个节点后用hermes peer list之类的命令查看peer状态确认连接已建立。连接本地模型时一个常见误区是把模型服务也当作一个peer节点加进来。模型服务不一定需要实现完整的hermes peer协议通常通过一个适配层转换。比如执行Agent在处理任务时会用HTTP或本进程内的SDK去请求本地模型拿到token后再把token组装成DATA帧通过hermes peer发给规划Agent。也就是说hermes peer解决的是Agent与Agent之间的通信Agent与模型之间反而可以用最朴素的方式因为那是进程内部的事。如果你用的是DeepSeek这类需要本地部署并注册进Agent生态的模型其实也是同一个思路。模型服务跑起来后你只需要让执行Agent的配置里指定模型API地址即可不需要为模型单独写一套peer接入逻辑。真正需要模型自身支持协议的情况是当你想让模型服务直接作为网络中的一个可通信节点这时可以在模型服务外层包一个hermes agent适配器。4.2 连接重置类错误速查表部署过程中最容易遇到的问题就是各种各样带connection reset by peer字样的报错。我把它们按出现位置和根因列成一个速查表报错原文常见根因排查手段curl: (35) recv failure: connection reset by peer客户端与服务端TLS握手或数据传输链路被中断先看服务端是否主动关闭再看中间设备是否有空闲超时最后校验TLS证书链是否完整error: protocol fault (couldnt read status): connection reset by peer服务端返回的数据格式不符合客户端预期客户端解析失败后关闭连接开启协议抓包对比握手和消息帧的字段重点看版本号、magic、length字段java.io.IOException: connection reset by peerJVM客户端在发送数据过程中对端关闭连接检查对端进程是否OOM或崩溃检查socket写缓冲区是否溢出stream disconnected before completion: io error: peer closed connection with...流式传输中途断掉核对流断点offset检查心跳是否提前超时查看对端负载情况看到curl: (35)时别急着怀疑应用代码。curl的35错误是指底层SSL连接建立失败或中途被重置优先排查网络设备和证书。如果你在调试环境里用curl模拟Agent之间的请求结果遇到这个错大概率是服务端根本没起来或者端口被防火墙拦了。protocol fault则要重点关注版本协商。这类报错常见于服务端升级后没有做向下兼容。hermes peer的握手阶段本来可以拦截这种问题但如果两边协议版本号不一致且配置成静默降级就会在解析后续帧时暴露矛盾。4.3 从实际运维中总结的稳定性调优部署稳定之后还有一个经常被忽略的点操作系统层面的连接参数。Agent节点之间每天要维持大量长连接Linux默认的tcp_keepalive_time是7200秒对Agent场景太长了。我会在部署文档里建议把内核参数调小比如net.ipv4.tcp_keepalive_time300这样在应用层心跳失效时内核还能兜底。另外不要把Socket的读写缓冲区设置得过大。之前为了追求吞吐我把SO_RCVBUF调大到16MB结果遇到慢消费者时数据全积压在内存里一旦进程OOM表现出的现象就是所有对端同时收到connection reset。后来改为在应用层做基于credit的流控缓冲区反而调小了一半。还有一点文件描述符上限。每个Agent节点都会与多个peer保持长连接如果节点内有线程池、连接池等占用fd默认的1024限制很快会打满。我把ulimit -n至少调到65535同时监控/proc/pid/fd的使用情况。5. 进阶让点对点通信更可靠的设计模式走到这一步基本功能已经通了但要把hermes peer用在生产环境还需要处理几个绕不开的可靠性问题。这里分享三个我反复验证过的设计模式重试策略、背压流控、消息幂等。5.1 重试策略哪些错误可以重试哪些不该碰不是所有错误都值得重试。重试能解决的是临时性故障比如网络抖动、对端进程暂时繁忙、连接被中间设备重置。重试不能解决的是确定性错误比如协议版本不匹配、鉴权失败、请求消息本身非法。在hermes peer里我会根据错误类型分类处理连接层错误connection reset、stream disconnected可重试但必须指数退避并加抖动。协议层错误protocol fault、unsupported frame不可重试立即上报排查代码bug。业务层错误参数缺失、上下文不存在由业务决定通常不需要重试而是触发Agent之间的协商流程。重试次数要设上限。我把默认最大重试次数设为5次每次间隔翻倍并加上随机0~50%的抖动。重试超过上限后消息不能被默默丢弃要进入死信队列或者触发告警。有一次我漏掉了这个环节结果一条失败消息被无限重试每个重试间隔还不同排查问题时看到几十条孤魂野鬼日志非常痛苦。还有一个容易犯的错重试时把整条消息原封不动发一遍。如果消息里带有一个时间戳或者一次性的nonce重试后对端可能因为nonce已被使用而报错。正确的做法是重试时更新消息头里的重试计数和trace_id但保留原始消息ID方便对端做去重。5.2 背压与流控别让慢消费者拖垮整个网络点对点通信最大的风险不是消息多了而是某个Agent处理不过来。如果不做流控发送方会把消息不断塞进TCP缓冲区接收方应用层来不及消费最终缓冲区写满连接被重置。hermes peer借鉴了TCP窗口的思路实现了基于credit的流控。接收方在握手和运行过程中会告知发送方当前可用的credit数量。每个DATA帧消耗一个credit接收方消费完一帧后才返还credit。发送方在credit耗尽时暂停发送等收到新的credit再继续。这个机制在流式场景下尤其有用。Agent A给Agent B推送大段token流B一边处理一边向模型服务请求输出。如果B的消费速度跟不上credit会被消耗完A自动暂停而不是继续猛推导致socket缓冲区爆掉。调credit大小时要平衡吞吐和延迟。credit太小发送方频繁等待吞吐上不去credit太大接收方内存压力大。我的经验是初始credit设为发送窗口的2倍左右运行中根据平均处理时延动态调整。5.3 消息幂等对端重复发你也不能重复做断线恢复依赖消息重发那接收方就会面临重复消息。如果执行Agent收到的任务消息重复了两次它可能把同一个工具调用执行两遍这在生产环境是不可接受的。解决思路很简单每条业务消息在创建时分配一个全局唯一的消息ID接收方在本地维护一个已处理消息ID的布隆过滤器或LRU缓存。重复消息直接丢弃并返回ACK。需要注意的是这里的消息ID应该是指logical message id而不是传输层的sequence。sequence在一个连接内有效而业务消息ID在Agent的整个生命周期内都要保证唯一。对于流式消息幂等判断可以细化到offset。接收方只处理大于当前消费位点的数据帧重复帧直接跳过。这也是为什么帧头要带sequence和流的offset。在大规模Agent网络里布隆过滤器的误判可能导致消息被当成重复而丢弃。所以我会在业务层再加一道应用层幂等键比如用任务ID加步骤ID组合成一个唯一键只有完全重复时才触发幂等避免过滤器的随机误差。5.4 最后一点经验如果你想在自己的项目里引入类似hermes peer的协议不要一上来就追求最全功能。先把握手、心跳、断线恢复这三个基础能力做好再去考虑流式续传、credit流控这些进阶特性。我最初就是先跑通了一个能跨进程发消息的JSON over TCP然后逐步加上协议版本协商才不至于在后期被兼容性问题绊倒。调心跳和超时参数时也一定要结合真实网络环境。本机两个进程通信心跳设5秒绰绰有余跨公网通信5秒可能太频繁了。我的建议是准备一套基准测试脚本在不同时延、丢包率下跑一遍看哪些参数组合能让系统的错误率最低。别只看成功路径的吞吐多看看失败路径的恢复时长这才是生产系统真正拉开差距的地方。