ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI Agent场景下WebSocket服务:原理、连接管理与高并发实战

AI Agent场景下WebSocket服务:原理、连接管理与高并发实战 做AI Agent开发的这半年我越来越认可一句话HTTP那套请求-响应模型放到Agent场景里就是拧巴。你要实时拿大模型的流式回复要推送工具调用状态要多端同步会话上下文——这些需求用HTTP硬做只能轮询、长轮询、SSE来回折腾连接管理一团乱麻。换用WebSocket之后一条长连接双向跑Agent和前端、Agent和Agent、Agent和编排框架之间的通信一下清爽了。这篇文章想聊的就是Agent场景下的WebSocket服务全景从HTTP为什么不够用到WebSocket握手原理、心跳机制、连接管理再到高并发下的稳定性实战最后把常见报错和排查思路一起列出来。适合正在做Agent服务端、做前端接入、或者手动搭Agent框架的兄弟们参考不涉及具体平台尽量把底层逻辑讲透。1. Agent为什么离不开WebSocketHTTP的单向壁垒1.1 Agent交互的三个反常特性先说一个反直觉的地方Agent不是传统意义上的“问答接口”。用户发一条消息之后Agent可能要做工具调用、查数据库、读文件、调大模型再根据结果继续推理。这个过程是异步的而且中间状态特别多——正在思考、正在调用工具、工具返回、正在生成回复每个状态用户都想知道。WebSocket正好就是为这种“服务端有话说就能随时说”的场景设计的。反观HTTP它天生是“客户端一问、服务端一答”。服务端没有办法主动把一条消息推到客户端。你说可以用客户端频繁轮询来模拟推送但Agent场景下一次完整任务可能持续十几秒甚至几分钟。如果每2秒轮询一次算下来一个用户一次对话就要产生几十个请求如果用户同时开着多个Agent会话光轮询请求就能把服务端压垮。这不是HTTP不优秀是它不合适。第二个特性是双向控制。用户看到Agent在跑一个长任务他不一定只想干等他还可能想中断、改参数、追加指令。WebSocket同一连接上用户能发消息服务端也能发消息双向直接对话。这个体验用HTTP做通常得开两个连接一个短轮询或者SSE接收状态一个POST发指令。两个连接还不好保证顺序经常出现“指令发了服务端还没收到前一条上下文”的错乱。第三个特性是会话连续性。Agent的多轮对话要维护上下文而Agent的编排进程本身就是一个有状态的长时运行单元。HTTP接口是无状态的每次请求都要拼参数、带sessionId、重建上下文非常繁琐。WebSocket连接天然带“连接即会话”的含义连接建立时的鉴权、会话绑定可以一次完成后续所有消息默认属于这个会话。1.2 轮询、长轮询、SSE都在绕路在进入WebSocket实战前先看一下大家在Agent项目里最常见到的几种替代方案以及它们各自的问题。轮询是最简单粗暴的。前端每隔几秒向服务端发一个“有结果了吗”的请求。实现很简单但问题也最明显实时性取决于轮询间隔间隔太短浪费带宽间隔太长用户感觉卡顿。最难受的是服务端大多数时候根本没有新数据白白烧了一堆并发请求和数据库压力。Agent场景如果你做的是多用户在线把并发都耗在空轮询上后面真实请求反而排队。长轮询是对轮询的改良客户端发请求之后服务端先hold住等有数据了再返回客户端收到后立刻发下一个请求。实时性比普通轮询好不少但本质上还是一次请求一次响应连接是断断续续的。在Agent这种高频消息推送场景每来一条消息就要重新走一遍HTTP建连和header解析效率很低。而且长轮询在Nginx网关下容易触达超时又要调一堆timeout参数很烦。SSE看起来是最接近的替代品服务端可以单向往客户端推消息还是基于HTTP兼容性好。做Agent的流式输出时很多人第一反应就是SSE。但SSE有一个致命短板客户端不能通过同一条连接给服务端发消息。在Agent场景里用户要“停止生成”“换一个工具”“修改参数”这些控制指令只能再开一条HTTP通道。两条通道容易乱序也多了不少复杂度。另外SSE在部分浏览器和代理环境下有连接缓冲限制实时性打折。用一张表总结这几个方案在Agent场景下的表现维度普通轮询长轮询SSEWebSocket实时性差取决于间隔较好但有延迟好单向最好双向即时双向通信支持支持不支持支持连接数量大量短连接大量半开连接一条长连接一条长连接服务端推送能力无只能等请求弱需hold强强Agent控制指令可以靠请求可以靠请求需另开通道同一条连接网关超时风险低高较高需心跳保活看完这个表你就明白为什么我的结论是Agent项目里如果消息频率高、需要双向交互直接上WebSocket如果只是简单把大模型的流式结果推给前端SSE也够用但千万别用SSE做双向Agent控制。1.3 WebSocket的本质升级后的全双工通道WebSocket不是一个全新的协议它巧妙地借用了HTTP的握手然后升级成独立的全双工协议。握手阶段客户端发送一个普通HTTP GET请求带上Upgrade: websocket和Sec-WebSocket-Key。服务端验证通过后返回101 Switching Protocols。从这一刻起这条TCP连接不再按“请求-响应”来约束客户端和服务端可以随时往连接里写数据。我常给团队打一个比方HTTP像寄快递你每次都要填单子、称重、打包拿到了包裹这个链路就结束了下次再寄又要重新来。WebSocket像通电话两边拨通之后谁想说话就什么时候说不用每次重新拨号。Agent场景里大模型每生成一个token、工具每次返回一个中间结果服务端就可以直接通过这条“电话线”推到前端完全不用等客户端来“领取”。这里还要澄清一个关键点WebSocket使用的是HTTP的握手端口通常是80/443但握手完成之后它不再受HTTP语义约束。这个设计让WebSocket可以轻松穿过防火墙和大部分代理因为代理看到的只是一次普通的HTTP升级。但反过来也说明代理如果配置不当可能会把“升级”请求按普通GET处理导致握手失败。这个问题后面第5节会细讲。2. WebSocket服务端搭建从握手到帧解析2.1 握手过程的关键细节虽然现在主流语言都有WebSocket库不需要自己写握手逻辑但理解握手细节对排查问题很有帮助。握手的核心是验证合法性客户端生成一段随机的Sec-WebSocket-Key服务端拿到后用固定GUID258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接再做SHA1哈希最后Base64编码得到Sec-WebSocket-Accept返回给客户端。如果这个值和浏览器本地计算的结果不一致浏览器会直接断开连接。这个设计的目的主要是防止普通的HTTP缓存代理把WebSocket握手请求当普通GET缓存成静态资源。如果不做校验一个存了响应缓存的代理可能把旧响应返回给新客户端连接就错乱了。服务端代码我在Node.js里常用ws库最小化实现如下const WebSocket require(ws); const wss new WebSocket.Server({ port: 8080 }); wss.on(connection, (ws, req) { console.log(客户端已连接:, req.url); ws.on(message, (data) { const msg JSON.parse(data.toString()); console.log(收到消息:, msg); // 回一条消息 ws.send(JSON.stringify({ type: ack, timestamp: Date.now() })); }); ws.on(close, () { console.log(连接关闭); }); });用浏览器直接连接const ws new WebSocket(ws://localhost:8080/agent/ws?sessionIdabc); ws.onopen () { ws.send(JSON.stringify({ type: ping })); }; ws.onmessage (event) { console.log(来自服务端:, event.data); };这个过程看起来简单但有个容易忽略的点URL里sessionId如果直接跟在query上会被Nginx日志和网关访问日志打出来容易泄露业务ID。更稳的做法是把sessionId放到Path里比如/agent/ws/abc再配合鉴权token放在Sec-WebSocket-Protocol头里。这样既方便路由又能减少日志泄露面。2.2 帧格式与消息边界WebSocket传输数据的基本单位是帧。每一帧包含控制信息和数据载荷控制信息里最关键的是FIN、opcode、mask和payload length。opcode决定帧类型文本帧是0x1二进制帧是0x2ping是0x9pong是0xA。FIN表示这一帧是不是消息的最后一帧。如果FIN0说明一个完整的业务消息被拆成了多帧客户端要拼接完才能解析。浏览器和大多数库会自动处理分片但如果你自己写协议解析或者用一些轻量级库就一定要处理分片状态否则收到的JSON是残缺的直接解析会报错。还有一个细节客户端发往服务端的帧必须加掩码mask服务端发往客户端的帧不要求掩码。这是协议明文规定的。有些自研网关没注意这个直接把收到的客户端数据转发给另一个客户端结果对方按无掩码解析数据全乱。同理如果你写了一个客户端SDK必须记得给发送帧加mask否则服务端会按协议错误断开连接。消息边界的问题在Agent场景特别明显。Agent一条系统消息可能几KB拆分后跨多个帧库虽然自动处理但你如果在中途把消息切给第三方服务比如丢进消息队列未等分片结束就发送接收方就会拿到半截JSON。我的建议是在WebSocket服务端入口统一做一次完整的“消息重组”重组完再往业务层丢不要等到业务层再处理。2.3 连接状态与优雅关闭长连接不能像HTTP那样直接断开TCP完事要走WebSocket协议里的关闭帧。客户端或服务端任一方向对方发送一个Close帧携带状态码1000表示正常关闭1001表示服务端关机1008表示策略违规等对方回一个Close帧然后TCP才关闭。这里最常见的坑是服务端在清理Agent会话时直接调用ws.terminate()强制拽断连接。虽然也能断开但客户端那边收不到Close帧只能看到“连接被异常断开”可能触发不必要的重连逻辑。更稳妥的做法是先ws.close(1000, agent session end)给客户端一个体面的退出信号让前端知道这是主动关闭不要再自动重试。另外服务端在关闭连接时要彻底清理连接管理器中的数据否则大量的close事件触发后Map里残留一堆引用内存泄漏会一点点积累。这个问题在长连接服务里尤其隐蔽我会在下一节详细讲连接管理器怎么做。3. Agent场景下的连接管理与消息推送3.1 连接管理器的设计Agent服务端不是只有一个WebSocket连接而是有成百上千个连接。每个连接对应一个会话、一个Agent实例或者一个用户。直接裸用WebSocket库的connection事件代码会很快失控所以我会在WebSocket之上加一层连接管理器。连接管理器要做三件事注册、心跳、清理。注册就是维护一个clientId - ws的映射。心跳负责周期检查和剔除死连接。清理是当连接关闭时把对应的Agent任务状态、会话资源一并释放。一个Node.js的示例class ConnectionManager { constructor() { this.connections new Map(); } add(clientId, ws) { // 如果已有老连接先关掉再换新的避免双连接 const old this.connections.get(clientId); if (old old.readyState WebSocket.OPEN) { old.close(1000, duplicate connection); } this.connections.set(clientId, ws); ws.on(close, () { if (this.connections.get(clientId) ws) { this.connections.delete(clientId); } }); } send(clientId, payload) { const ws this.connections.get(clientId); if (ws ws.readyState WebSocket.OPEN) { ws.send(JSON.stringify(payload)); return true; } return false; } broadcast(type, payload) { const msg JSON.stringify({ type, payload }); this.connections.forEach((ws) { if (ws.readyState WebSocket.OPEN) { ws.send(msg); } }); } }这个类看起来简单但有一个容易被忽略的细节重复连接的覆盖。Agent用户可能在多个标签页打开同一个会话旧的连接如果不主动关闭消息就会乱。比如旧连接用sessionId A连上新连接也用sessionId A连上服务端如果不把旧连接踢掉用户会看到消息在旧标签页刷新、新标签页收不到。所以连接管理器要在加入新连接时检查旧连接。3.2 心跳机制为什么是刚需很多Agent开发者都会迷惑我的WebSocket连接明明好好的过一会儿就自动断了Nginx日志里也没看到错误。十有八九是没做心跳。问题根源在于WebSocket连接是长连接而网络中间层Nginx、云负载均衡、运营商网关通常会为“空闲连接”设一个超时时间。默认情况下Nginx的proxy_read_timeout是60秒如果60秒内这条连接上没有数据流动代理层就会主动把它断开。你可能觉得WebSocket建连后应该一直活着但对代理层来说“没有数据流动”的TCP连接是资源浪费是可以回收的。心跳就是用来伪造“数据流动”的。最简单的办法是服务端每隔一段时间发一个ping帧客户端自动回pong帧。浏览器原生WebSocket虽然不能手动发ping但收到ping会自动回pong所以服务端只需要定时ping就能保证连接上有流量。这里有三个关键参数需要注意心跳间隔要小于代理超时时间的一半。Nginx如果默认60秒超时心跳至少30秒一次建议25秒留足余量。服务端发ping之后要记录该连接的最后pong时间超过阈值就主动close。心跳和业务消息不能冲突要区分类型客户端收到ping不要当业务消息处理。服务端实现// 每30秒扫描一次所有连接 const HEARTBEAT_INTERVAL 30 * 1000; const MAX_PONG_WAIT 15 * 1000; setInterval(() { connectionManager.connections.forEach((ws, clientId) { if (ws.readyState ! WebSocket.OPEN) return; if (ws.isAlive false) { ws.terminate(); connectionManager.delete(clientId); return; } ws.isAlive false; ws.ping(); // 15秒内没有收到pong则自动被置为false }); }, HEARTBEAT_INTERVAL);前端要配合处理重连。如果前端发现连接断开或者一段时间没收到任何消息就要主动重连。重连时最好带上指数退避避免所有用户同时断线重连造成服务端压力峰值。3.3 Agent流式输出的服务端推送Agent调用大模型时的流式输出是WebSocket在Agent场景下最典型的价值。传统HTTP接口要等大模型完整生成完才返回用户看到的就是长时间白屏用WebSocket服务端把上游流式的token一个个转发到前端用户可以看到打字机效果。服务端的逻辑大致是前端发一条start_task消息携带任务ID和参数。Agent服务端收到消息后启动大模型调用拿到一个可读流。服务端遍历这个可读流每读到一段文本就通过WebSocket发一条task_stream消息。全部完成后发一条task_complete消息附带最终结果。伪代码大概是ws.on(message, async (data) { const msg JSON.parse(data); if (msg.type start_task) { const stream await callLLM(msg.params); for await (const chunk of stream) { if (ws.readyState ! WebSocket.OPEN) break; ws.send(JSON.stringify({ type: task_stream, taskId: msg.taskId, content: chunk })); } ws.send(JSON.stringify({ type: task_complete, taskId: msg.taskId, data: result })); } });这个实现有一个需要特别处理的问题客户端中断。用户看到某个tool调用结果不满意点了“停止”前端发一条cancel_task消息服务端如果还傻傻地继续遍历上游流就会产生大量无用的生成浪费token。所以服务端必须在收到cancel_task时及时中断上游流并清理相关任务资源。具体做法是使用AbortController在for await循环内判断信号收到取消信号后主动break。我踩过这个坑有一次用户取消后大模型调用已经发出去了服务端虽然不再转发但上游生成还在继续账单照跑。后来我在调用大模型SDK时直接传入signal取消就立刻断开上游连接这个问题才彻底解决。3.4 多Agent之间的消息路由Agent和Agent之间的通信严格来说不一定要走WebSocket因为Agent服务端之间通常内网互通用消息队列更合适。但如果你在做一个本地开发调试工具或者一个可视化的Agent编排平台想实时把Agent状态推给前端并让前端操作AgentWebSocket反而是更轻量的方案。这种情况下我会把WebSocket服务端设计成一个轻量消息路由。每个Agent实例启动后用它自己的agentId建一个WebSocket连接前端也用同一个服务建一个连接。服务端维护agentId - ws和userId - ws两个映射同一边连接起来的不需要走HTTP所有消息都从服务端转发。比如Agent A要调用工具X它把请求发到WebSocket服务端服务端再转发给用户前端显示“Agent A正在调用工具X”。这个路由也需要一个消息类型字段我用的是kindfunction route(ws, msg) { switch (msg.kind) { case agent_to_user: connectionManager.send(msg.userId, { from: msg.agentId, ...msg }); break; case user_to_agent: connectionManager.send(msg.agentId, { from: msg.userId, ...msg }); break; case agent_to_agent: connectionManager.send(msg.targetAgentId, { from: msg.agentId, ...msg }); break; default: ws.send(JSON.stringify({ error: unknown kind })); } }这里有一个重要的经验不要在两个Agent之间直接建立WebSocket连接。因为Agent数量一多连接数是平方级增长的。正确的做法是星形连接所有连接都连到一个中心服务由中心做路由。中心服务一方面可以做权限校验另一方面可以统一监控和限流避免Agent之间互相刷消息把网络打满。4. 高并发与稳定性扛得住才是硬道理4.1 连接数冲击下的内存评估Agent服务一上线最现实的问题就是能扛多少并发。WebSocket是长连接每个连接都要占用内存。不同语言、不同框架下单条连接的内存占用不同。以Node.js为例一条WebSocket连接包含TLS大约占用几十KB到一百多KB。一个简单估算假设单连接平均50KB那么1万连接就是500MB10万连接就是5GB。这还不包括连接管理器里的业务对象和Agent会话上下文。所以我在设计Agent服务时不会一上来就追求“无限并发”而是先做容量评估预估同时在线数 × 单连接内存 Agent会话内存 × 并发会话数 内存预算。如果一个普通4GB服务器想扛5万连接就算纯连接内存能凑合Agent上下文的叠加也可能直接吃满。这时候就要考虑分布式部署了。避免单个节点被打爆有两个好习惯在所有WebSocket服务入口加连接数上限超出后直接拒绝新建连接或排队等待。对不活跃的Agent会话做超时回收不能因为用户没有关闭页面就让连接永远挂着。“AI Agent怎么扛并发”这个问题表面是技术参数背后其实是资源预算。先把容量算清楚再优化代码不然调一堆参数也是白搭。4.2 心跳频率与资源消耗的平衡心跳能保活连接但如果做得太频繁在高并发下会变成另一种压力。我以前见过一个团队把心跳间隔设成5秒连接数到了2万光心跳流量每秒钟就是几千个ping/pong白白消耗了带宽和CPU。合理的心跳频率取决于你的网络链路和代理超时配置不是越快越好。一个简单经验公式心跳间隔 代理超时时间 / 3。如果Nginxproxy_read_timeout是60秒心跳间隔设为20秒如果网关超时是120秒心跳可以放慢到40秒这样心跳流量降到一半。同时要注意心跳消息不要用业务JSON格式直接用WebSocket协议层的ping/pong帧。业务JSON体积大解析开销也大。协议层的ping帧只有几个字节客户端浏览器内核自动处理pong完全不需要手动改业务代码这是性价比最高的方案。4.3 断线重连的幂等设计Agent和普通IM不一样。用户断线重连之后Agent任务可能还在后台跑也可能已经跑完了。如果前端重连后不管三七二十一重新发一遍start_task任务就重复执行了可能重复调用支付接口、重复发消息、重复改数据库。所以WebSocket重连必须做幂等。一个实用方案是每次创建任务时生成一个taskId前端发送start_task时带上clientMessageId或者taskId。服务端收到消息后先检查这个taskId是否已经存在如果不存在创建新任务。如果已存在且还在执行忽略本次start_task只补发当前进度。如果已存在且已经完成直接把最终结果再推一次。前端重连后不需要主动补发任务只需要发一条sync_task_status带上它自己记录的任务列表。服务端把每个任务的最新状态拉出来推给它。这样即使中间丢了消息重连后也能恢复一致性。前端断线重连代码示例function connectWebSocket() { const ws new WebSocket(ws://localhost:8080/agent/ws/${sessionId}); let retry 0; ws.onclose () { const delay Math.min(1000 * Math.pow(2, retry), 10000); retry 1; setTimeout(connectWebSocket, delay); }; ws.onopen () { retry 0; ws.send(JSON.stringify({ type: sync_task_status, tasks: myTaskList })); }; }重连间隔用指数退避最大不超过10秒既避免频繁重试也不会让用户等太久。这里面还有一个细节不要把重连逻辑写在onerror里否则网络抖动时会触发多次重连反而加重服务端压力。只监听onclose等连接彻底关闭后再重连。4.4 多实例部署的连接一致性问题当单个WebSocket服务扛不住连接数就得横向扩展多实例部署。这时会遇到一个新的问题同一个用户连接的是不同实例但Agent的任务状态可能只存在其中一台机器上。如果这台机器挂了用户的WebSocket就被断了即使另一台实例能接受连接也拿不到原始会话。常见的解法有两种。第一种是Sticky Session。负载均衡层通过IP Hash或Cookie把同一个用户固定到同一台实例。这个方案简单适合中小规模也能保住会话内存。代价是做多实例时如果不处理容灾某台实例一挂落在它上面的用户全部掉线还不能转移会话。第二种是外部存储 发布订阅。把Agent会话状态放到Redis每个实例维护自己本地的WebSocket连接。实例之间通过Redis Pub/Sub同步消息。比如用户A连接在实例1用户B连接在实例2Agent A要给用户B发消息实例1把消息publish到Redis channel实例2收到后转发给用户B。这个方案把“连接位置”和“业务状态”解耦扩展性最好。第二种方案的搭建成本高一些但对Agent这种有状态、长连接、多实例的场景是更稳妥的做法。实际项目里我先用Sticky Session快速上线等用户规模上来后再平滑迁移到Redis Pub/Sub避免一开始过度设计。5. 实战踩坑记录与问题速查5.1 握手请求头过大导致400有一次同事反馈Agent调试页面打开后WebSocket连不上报错是HTTP error 400. A request header field is too long。我一开始怀疑是服务端证书问题后来抓包发现问题出在握手请求携带的Cookie上。因为前端把所有用户信息都塞进了Cookie导致总共几个KB的Cookie直接让Nginx报错。排查思路先看请求头总体大小再逐项排查。服务端对请求头大小有限制Nginx默认large_client_header_buffers是4个8KB所以单个请求头不能超过8KBNode.js默认maxHeaderSize是16KB。如果整体超了也会直接被拒。解决办法有三条路压缩Cookie把不需要的前端状态移到localStorage只在Cookie里留sessionId。把token放到Sec-WebSocket-Protocol头但这个头也有长度限制不能放太多。调大服务端header限制。我个人强烈推荐第一条。因为这不是为WebSocket调大的问题即使现在调大了以后Cookie还会继续膨胀迟早变成隐患。不如趁早从数据上做减法。5.2 连接假死与心跳失效另一个常见问题是客户端看着连接是绿色的但服务端已经把它判定为死连接了。最典型的情况是用户电脑休眠或者网络切换TCP连接在客户端那边已经断了但服务端不知道因为TCP没有主动通知机制。我在一个Agent管理后台遇到过用户开着浏览器挂着页面睡了一觉回来页面显示“在线”但Agent状态怎么刷都更新不了。后来发现是心跳逻辑写错了——服务端发ping后没有检查pong超时客户端虽然已离线服务端却一直把它的连接当成有效连接继续往里面发消息。正确的做法是服务端记录每个连接的最后pong时间心跳检测时不仅发ping还要检查超过阈值未回复pong的连接直接terminate()并清理业务状态。我在前面第3节已经给了示例这里不再重复。核心就是发ping是手段收不到pong就是结论不要给假死连接留任何空间。5.3 代理层没配置Upgrade导致握手失败WebSocket握手的HTTP升级请求如果经过Nginx代理而Nginx配置里没有设置Upgrade相关头连接会在代理层就断掉。症状是浏览器控制台显示WebSocket握手失败返回404或者400但直接访问服务端IP又是好的。Nginx里正确的WebSocket代理配置一般是这样的location /agent/ws/ { proxy_pass http://backend_ws; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 300s; proxy_send_timeout 300s; }重点就是第三行和第四行。proxy_http_version必须设为1.1Connection必须显式设置为upgrade否则Nginx不会把后续连接数据透传到后端。proxy_read_timeout和proxy_send_timeout建议调大配合前端心跳。如果这两个超时设成默认60秒就算WebSocket连接本身没断代理层也会把空闲连接杀掉。5.4 HTTP连接复用与WebSocket的误区热词里能看到很多人在搜“http连接复用”和“WebSocket服务”说明不少人把HTTP Keep-Alive和WebSocket搞混了。HTTP Keep-Alive只是让TCP连接可以复用它依然是“客户端发起一个请求、服务端返回一个响应”服务端不能主动往这条连接里塞消息。所以Keep-Alive解决的是连接重建的开销解决不了单向壁垒。WebSocket则是把这条连接从“请求-响应”模式升级成了“任意时刻双向收发”模式它是协议层面的变化不是简单的连接复用。这个误会在Agent场景里容易导致错误决策有人为了省事用HTTP Keep-Alive加上轮询表面看“连接是长久的”其实消息推送还是轮询拿的实时性并没有提升。我的建议是做Agent服务端时把需求先分清楚如果只需要服务端单向通知SSE更轻量如果需要双向交互直接用WebSocket只在“复用请求”上打转根本解决不了“服务端主动推消息”的核心问题。5.5 常见问题速查表问题现象可能原因解决方向握手失败返回400请求头过大服务端header限制压缩Cookie调大header限制握手失败返回404Nginx代理未配置Upgrade添加Upgrade与Connection头连接建好后几十秒自动断代理空闲超时添加心跳机制缩短心跳间隔客户端显示在线但收不到消息服务端未检测假死连接记录pong超时主动terminate消息丢失或乱序分片未重组在服务端入口统一重组消息重连后任务重复执行幂等逻辑缺失用taskId去重补发任务状态平时排障我习惯抓包看WebSocket帧而不是只看业务日志。WebSocket的ping/pong、close、分片在业务日志里不一定完全体现。抓包能直接看到协议层发生了什么很多看起来莫名奇妙的问题其实都是协议层细节没处理好。6. 说点个人经验如果现在让我重新做一个Agent项目我不会一上来就全站上WebSocket。我会先想清楚是谁给谁发消息频率多高需不需要双向控制。如果只是把大模型结果推给前端SSE够用如果要做一个会话式的Agent调试台要展示工具调用过程、要支持用户中断、要实时修改参数那WebSocket就是正解。还有一个亲身经验WebSocket服务的日志一定要和普通HTTP接口日志分开。Agent场景消息量很大如果混在一个日志系统里排障时要翻半天而且普通接口日志会把WebSocket的帧信息冲掉。分开之后WebSocket服务单独记连接生命周期和消息类型统计一查一个准。最后分享一个小技巧开发阶段用Chrome DevTools里的Network面板可以看到WebSocket帧列表非常直观。部署之后可以用命令行工具wscat做连接测试也可以用Node脚本模拟客户端发消息比每次打开浏览器快得多。把这些工具用熟WebSocket的调试效率能提升一个台阶。
返回列表