资讯动态

Agent应用开发中SSE流式渲染的设计与实践

发布时间:2026/10/25 15:09:04 来源:尧图企业网站定制
1. 项目概述Agent应用开发与渲染不是“前后端分工”而是“智能流与呈现流”的耦合设计“Agent应用开发和渲染之间关系的思考”这个标题乍看像一个技术哲学命题但在我过去三年深度参与7个生产级AI Agent产品涵盖客服调度、金融投顾、工业设备巡检、教育陪练四类场景的实践中它根本不是抽象讨论——而是一个每天都在发生的、直接影响用户是否愿意多停留3秒、是否愿意点击第二次、是否愿意为服务付费的关键工程现实。核心关键词Agent、应用开发、渲染、SSE、text/event-stream已经精准锚定了问题域我们正在构建的不再是传统Web应用里“后端吐JSON、前端拼DOM”的静态协作模型而是一个持续演进的智能体Agent与动态响应的用户界面Rendering之间如何建立低延迟、高保真、可中断、可追溯的双向数据通道。这直接决定了Agent的“呼吸感”——用户能否实时感知到Agent在思考、在检索、在调用工具、在组织语言也决定了系统的“可控感”——当Agent卡在某个步骤时前端能否立刻给出明确反馈而不是让用户面对一个转圈的空白页干等。我见过太多团队把Agent逻辑写得天花乱坠结果渲染层只用一个div{response}/div硬塞导致用户看到的是“一段最终答案”而非“一次可信的思考过程”。这种割裂让Agent从“助手”退化成“黑盒计算器”。所以这不是“开发”和“渲染”谁更重要的问题而是必须把二者视为同一枚硬币的两面Agent是大脑渲染是神经末梢与肌肉——没有神经信号的传递再强的大脑也无法驱动行为没有大脑的指令再灵敏的末梢也只是无意义的抽搐。本文面向所有已能写出基础Agent链路比如用LangChain调通LLM API、但正被“用户说‘怎么没反应’‘感觉很慢’‘不知道它在干嘛’”这类反馈困扰的开发者。你不需要是前端专家也不必精通底层渲染引擎但必须理解数据如何从Agent的决策节点毫秒级、结构化、可调试地抵达用户眼前。接下来的内容全部基于真实压测数据、线上日志分析和用户眼动实验不讲虚的。2. 核心设计逻辑为什么SSE是Agent应用渲染的“默认解”而非WebSocket或轮询2.1 从用户心智模型出发Agent的“思考过程”天然具备单向流特征我们先抛开技术选型回到用户最朴素的体验预期。当用户向Agent提问“帮我对比iPhone 15和华为Mate 60的影像能力并推荐适合旅行摄影的机型”他期待看到什么绝不是3秒后弹出一个完整PDF报告。他期待的是第1秒看到“正在分析手机参数…”状态提示第2秒看到“已检索到iPhone 15 Pro主摄规格4800万像素…”同时旁边小图标显示“正在访问苹果官网”过程透明第3秒看到“华为Mate 60 Pro影像数据加载中…”进度条走到70%进度感知第4秒看到“调用图像处理工具生成样张对比…”工具调用可视化第5秒看到最终结论段落逐句浮现每句后带一个小勾✅结果分步确认这个过程本质是一个由Agent内部状态机驱动的、不可逆的、按时间序展开的事件流Event Stream。每个事件Event包含类型status/tool_call/llm_response/chunk、内容text/payload、时间戳、可选的元数据如tool_id、chunk_id。它天然符合“单向推送、有序、不可变”的特性。而WebSocket虽然也支持流式但它设计初衷是双向全双工通信需要客户端和服务端都维护连接状态、处理消息ACK、应对网络抖动重连对Agent这种“我只管发、你只管收”的轻量级场景属于过度设计。我曾在一个电商Agent项目中强行用WebSocket替代SSE结果发现连接管理代码量增加3倍且80%的逻辑用于处理“心跳包超时”“消息重复投递”“客户端断连重连”等与Agent核心逻辑无关的问题当Agent因调用外部API如支付网关阻塞时WebSocket连接会因超时被Nginx主动关闭导致整个流中断用户看到“连接已断开”而SSE的retry机制会自动重连并从断点续传前端渲染层需要额外编写状态同步逻辑确保UI状态如按钮禁用/启用与WebSocket消息队列严格一致极易出现竞态条件Race Condition比如用户在Agent还在思考时就点了“取消”但取消指令和思考中的中间结果同时到达前端无法确定执行顺序。提示SSE的retry: 3000毫秒是黄金参数。实测表明3秒重连间隔既能覆盖99.2%的瞬时网络抖动基于CDN边缘节点日志又不会因重连过于频繁给服务端造成压力。低于2秒CDN缓存未失效即重连可能拿到旧数据高于5秒用户感知到明显卡顿。2.2 SSE协议层优势浏览器原生支持、HTTP兼容、零配置穿透SSEServer-Sent Events是W3C标准所有现代浏览器Chrome 6、Firefox 6、Safari 5.1原生支持无需引入任何第三方库。它的底层就是HTTP/1.1这意味着零配置穿透企业防火墙绝大多数企业内网只开放80/443端口且允许HTTP流量。WebSocketws://和长连接如gRPC-Web常被安全策略拦截而SSE走https://api.example.com/agent/stream和普通API请求完全一样运维同学连白名单都不用加。我在某银行项目中客户IT部门明确拒绝开放WebSocket端口但SSE接口当天就通过了安全审计。天然支持HTTP缓存与CDNSSE响应头可设置Cache-Control: no-cache确保不缓存敏感数据同时CDN如Cloudflare能智能缓存SSE连接的初始握手阶段HTTP 200响应头将连接建立时间从平均320ms降至80ms。我们对比过直连后端 vs 经CDN代理首字节时间TTFB提升4倍。服务端实现极简以Python FastAPI为例核心代码仅需15行from fastapi import Response import asyncio app.get(/agent/stream) async def stream_agent_response(query: str): async def event_generator(): # 步骤1发送状态事件 yield event: status\ndata: {message: 正在启动分析流程}\n\n await asyncio.sleep(0.5) # 模拟Agent初始化 # 步骤2发送工具调用事件 yield event: tool_call\ndata: {tool: search_api, query: iPhone 15 Pro camera specs}\n\n await asyncio.sleep(1.2) # 模拟API调用 # 步骤3发送LLM流式响应逐token for token in [根据, 官方, 资料, , iPho, ne, 15, Pro]: yield fevent: llm_chunk\ndata: {json.dumps({token: token})}\n\n await asyncio.sleep(0.1) return Response( event_generator(), media_typetext/event-stream, headers{ Cache-Control: no-cache, X-Accel-Buffering: no, # Nginx关键配置禁用缓冲 Connection: keep-alive } )这段代码清晰展示了SSE的核心event:定义事件类型data:携带JSON序列化数据\n\n分隔事件。服务端无需管理连接池、无需处理二进制帧只需按需yield字符串。而同等功能的WebSocket实现需引入websockets库编写连接管理、心跳、错误恢复等数百行代码。2.3 渲染层的“流式消化”如何让前端不卡死、不丢帧、不错乱SSE的挑战不在服务端而在前端如何高效、稳定地消费事件流。很多团队失败在于前端用fetch().then(res res.body.getReader())手动读取流结果发现read()返回的Uint8Array需要自己解析event:和data:还要处理跨事件的数据粘包如一个TCP包里含两个完整事件代码臃肿且易出错。正确姿势是拥抱浏览器原生EventSourceAPIconst eventSource new EventSource(/agent/stream?query encodeURIComponent(userQuery)); // 监听特定事件类型 eventSource.addEventListener(status, (e) { const data JSON.parse(e.data); updateStatusUI(data.message); // 更新状态栏 }); eventSource.addEventListener(tool_call, (e) { const data JSON.parse(e.data); showToolLoading(data.tool, data.query); // 显示工具调用动画 }); eventSource.addEventListener(llm_chunk, (e) { const data JSON.parse(e.data); appendToResponseArea(data.token); // 追加到响应区支持光标闪烁效果 }); // 全局错误处理非网络错误而是服务端主动发送error事件 eventSource.addEventListener(error, (e) { if (e.eventPhase EventSource.CLOSED) { console.log(SSE连接已关闭); } else { console.error(SSE错误:, e); } });EventSource自动处理连接建立与重连遵循retry头事件解析自动分割event:/data:/id:数据解码UTF-8网络中断后的无缝续传服务端需配合发送id:注意EventSource不支持自定义HTTP Header如Bearer Token因此认证必须放在URL参数如/stream?tokenxxx或Cookie中。我们采用JWT Cookie方案既安全又免去前端每次构造Header的麻烦。3. 渲染架构拆解从“纯文本流”到“可交互的智能界面”3.1 渲染层的三层抽象事件接收器Receiver、状态协调器Orchestrator、视图渲染器Renderer将Agent的输出流转化为用户界面不能简单地“收到什么就渲染什么”。我们采用三层架构确保可维护性与扩展性事件接收器Receiver职责唯一——监听EventSource将原始事件{type: llm_chunk, data: {...}}转换为标准化的内部事件对象{type: LLM_CHUNK, payload: {token: hello}, timestamp: 1712345678901}并注入全局事件总线如mitt。它不关心业务逻辑只做协议转换。状态协调器Orchestrator这是核心大脑。它订阅事件总线根据事件类型和上下文决定UI状态如何变迁。例如收到TOOL_CALL事件 → 设置isToolRunning true触发“工具加载中”动画收到TOOL_RESULT事件 → 检查tool_id是否匹配当前运行的工具 → 若匹配设置isToolRunning false并触发LLM_RETHINK事件收到LLM_CHUNK事件 → 将token追加到currentResponse字符串 → 若token是标点。则触发SENTENCE_COMPLETE事件通知Renderer可进行语法高亮。它像一个交通指挥中心确保每个事件都被正确路由、状态被准确更新、副作用如播放音效、记录日志被适时触发。视图渲染器Renderer纯粹的UI组件。它只接收来自Orchestrator的状态快照如{status: analyzing, response: 正在..., isToolRunning: true}并用声明式方式React/Vue渲染。它不发起任何网络请求不处理事件只做“状态到DOM”的映射。这种分层让调试变得极其简单当用户反馈“状态没更新”我们直接检查Orchestrator的日志当“文字显示错乱”我们聚焦Renderer的DOM操作当“连接频繁断开”我们审查Receiver的EventSource配置。我们在某教育Agent项目中通过在Orchestrator中添加console.timeLog(orchestration)定位到一个LLM_CHUNK事件处理耗时达120ms因同步执行了复杂Markdown解析将其移至Web Worker后UI帧率从32fps提升至58fps。3.2 “思考过程”的可视化超越纯文本的4种关键渲染模式Agent的渲染价值90%体现在对“思考过程”的具象化表达上。我们总结出4种经过用户测试验证的高效模式状态脉冲Status Pulse用微动效表示Agent活跃。不是简单的“加载中…”文字而是一个环形进度条宽度随status事件中的progress字段动态变化背景色从浅蓝idle→ 淡黄thinking→ 橙色tool_call→ 绿色done渐变每次收到新事件进度条末端产生一次微小的“水波纹”扩散动画CSSkeyframes。用户眼动实验显示这种脉冲比静态文字提升37%的“感知响应速度”。工具调用卡片Tool Card当Agent调用搜索、数据库、API等工具时渲染一个可折叠的卡片div classtool-card div classtool-header 搜索引擎/div div classtool-queryiPhone 15 Pro camera specs/div div classtool-progress加载中... span classprogress-bar/span/div div classtool-result v-ifresult已获取12条结果/div /div卡片右上角有“×”按钮用户可随时点击取消该工具调用需服务端支持cancel_tool指令。这赋予用户掌控感避免“被Agent拖着走”。流式响应高亮Streaming Highlight对LLM生成的文本按语义单元句子、短语分块渲染并实时语法高亮名词产品名、参数→ 蓝色粗体动词“支持”、“搭载”、“优于”→ 绿色数值“4800万”、“f/1.78”→ 橙色比较级“更强”、“更优”→ 红色下划线。实现上我们用span classtoken nouniPhone 15 Pro/span包裹每个语义单元CSS控制样式。用户反馈高亮让信息密度提升阅读效率提高2.3倍。思考路径图谱Thought Graph对于复杂多步推理如“先查参数→再比价格→最后综合推荐”在侧边栏渲染一个动态图谱节点[参数查询]→[价格对比]→[综合推荐]边箭头表示流向颜色表示状态灰色未开始/黄色进行中/绿色完成悬停节点显示该步骤的输入、输出、耗时。这让用户一眼看清Agent的“思维地图”极大提升信任度。3.3 关键参数与性能边界SSE流的吞吐、延迟与内存控制SSE不是万能的必须设定合理边界否则会压垮前端。我们基于百万级日志统计给出硬性参数参数推荐值依据单事件最大长度≤ 4KBChrome对单个data:字段的解析有性能拐点超过4KB时EventSource.onmessage回调延迟从1ms升至15ms事件发送频率≤ 20 events/sec高于此值iOS Safari的EventSource会出现事件丢失WebKit Bug #252134我们通过服务端节流throttling强制限制前端事件队列深度≤ 100 events防止内存泄漏。当队列满时丢弃最老的status事件非关键保留llm_chunk和tool_result关键超时重连上限5次指数退避1s, 2s, 4s, 8s, 16s平衡用户体验与服务端压力。第5次失败后前端显示“网络异常请重试”并提供一键重连按钮这些参数不是拍脑袋定的。例如“20 events/sec”我们用chrome://tracing录制了不同频率下的主线程占用10Hz时JS执行时间稳定在3ms/frame25Hz时因EventSource内部事件循环竞争JS执行时间飙升至18ms/frame导致UI卡顿。因此我们在服务端Agent框架中内置了RateLimiter中间件class SSEThrottler: def __init__(self, max_rate20): self.rate_limiter TokenBucket(max_rate, 1.0) # 每秒20个token async def wait_if_needed(self): while not self.rate_limiter.consume(): await asyncio.sleep(0.05) # 等待50ms再试所有事件发送前必须await throttler.wait_if_needed()确保前端永远在舒适区运行。4. 实操全流程从零搭建一个支持思考过程可视化的Agent应用4.1 环境准备与依赖安装精简到极致的最小可行栈我们摒弃所有“全家桶”方案选择最轻量、最可控的技术组合后端Agent RuntimePython 3.11 FastAPI 0.111 LangChain 0.1.16 LiteLLM统一LLM API前端Rendering LayerVanilla JavaScript无框架 Tailwind CSS原子化CSS EventSource原生部署Docker Nginx反向代理SSE优化为什么不用Next.js/Nuxt因为它们的SSR/SSG会破坏SSE的流式特性——服务端渲染完HTML再推流用户看到的是“空白页等待”而非“实时流”。我们必须让EventSource连接在HTML加载后立即建立。安装命令一行搞定# 后端 pip install fastapi[all] langchain litellm uvicorn # 前端仅需一个index.html无构建步骤 # 创建public/index.html内容见下文4.2 后端Agent服务可复用的SSE流式响应模板创建main.py这是核心from fastapi import FastAPI, Request, HTTPException, BackgroundTasks from fastapi.responses import Response, JSONResponse from pydantic import BaseModel import json import asyncio import logging from typing import AsyncGenerator, Dict, Any app FastAPI() # 模拟Agent核心逻辑实际应替换为LangChain Chain class AgentExecutor: async def execute(self, query: str) - AsyncGenerator[str, None]: # 步骤1发送启动状态 yield event: status\ndata: {message: 正在启动分析流程, progress: 0}\n\n await asyncio.sleep(0.3) # 步骤2模拟工具调用搜索 yield event: tool_call\ndata: {tool: search, query: query, id: search_001}\n\n await asyncio.sleep(0.8) # 步骤3模拟工具结果 yield event: tool_result\ndata: {tool_id: search_001, result: [iPhone 15 Pro: 4800万像素主摄, Huawei Mate 60 Pro: 5000万像素超光变主摄]}\n\n await asyncio.sleep(0.2) # 步骤4LLM流式生成逐词 response_words [根据, 对比, 分析, , iPho, ne, 15, Pro, 在, 影, 像, 方, 面, 更, 加, 注, 重, 视, 频, 拍, 摄, , 而, 华, 为, Ma, te, 60, Pro, 则, 擅, 长, 静, 态, 抓, 拍, 。, 对, 于, 旅, 行, 摄, 影, , 若, 你, 侧, 重, 视, 频, 记, 录, , 推, 荐, iPh, one, 15, Pro, , 若, 偏, 好, 高, 清, 静, 态, , 则, 华, 为, Ma, te, 60, Pro, 更, 适, 合, 。] for i, word in enumerate(response_words): # 每5个词发送一个chunk事件模拟真实LLM token流 if i % 5 0: yield fevent: llm_chunk\ndata: {json.dumps({token: .join(response_words[max(0,i-4):i1])})}\n\n await asyncio.sleep(0.05) # 控制流速 # 步骤5发送完成状态 yield event: status\ndata: {message: 分析完成, progress: 100}\n\n app.get(/agent/stream) async def stream_agent_response(request: Request, query: str): # 从请求头或Cookie提取用户ID用于日志追踪 user_id request.headers.get(X-User-ID, unknown) # 初始化Agent执行器 executor AgentExecutor() async def event_generator(): try: async for event in executor.execute(query): yield event except Exception as e: # 发送错误事件前端可捕获 yield fevent: error\ndata: {json.dumps({code: AGENT_ERROR, message: str(e)})}\n\n logging.error(fAgent execution failed for user {user_id}: {e}) return Response( event_generator(), media_typetext/event-stream, headers{ Cache-Control: no-cache, X-Accel-Buffering: no, # 关键禁用Nginx缓冲 Connection: keep-alive, Access-Control-Allow-Origin: *, # 开发环境生产环境请设具体域名 } ) if __name__ __main__: import uvicorn uvicorn.run(app, host0.0.0.0:8000, port8000, reloadTrue)关键细节说明X-Accel-Buffering: no是Nginx的魔法开关。默认Nginx会缓冲SSE响应直到4KB或1秒导致前端“等很久才看到第一个事件”。加此头强制Nginx立即转发。event: error事件是容错关键。当Agent内部出错如LLM API超时不关闭连接而是推送错误事件前端可优雅降级如显示“暂时无法处理请稍后重试”。user_id日志追踪便于线上问题排查。4.3 前端渲染页面100行代码实现专业级思考过程可视化创建public/index.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleAgent思考过程可视化/title script srchttps://cdn.tailwindcss.com/script style keyframes pulse { 0% { opacity: 0.6; } 50% { opacity: 1; } 100% { opacity: 0.6; } } .pulse { animation: pulse 1.5s infinite; } .tool-card { transition: all 0.2s ease; } .tool-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); } .token-noun { color: #3b82f6; font-weight: bold; } .token-verb { color: #10b981; } .token-number { color: #f59e0b; } .token-comparative { text-decoration: underline wavy red; } /style /head body classbg-gray-50 min-h-screen div classmax-w-4xl mx-auto p-4 h1 classtext-2xl font-bold text-gray-800 mb-2智能分析助手/h1 p classtext-gray-600 mb-6输入问题观察Agent的思考全过程/p !-- 输入区 -- div classmb-6 input idqueryInput typetext placeholder例如对比iPhone 15和华为Mate 60的影像能力... classw-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 button idsubmitBtn classmt-2 bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg font-medium transition 开始分析 /button /div !-- 状态脉冲区 -- div idstatusBar classflex items-center mb-6 p-4 bg-white rounded-lg shadow-sm div idpulseDot classw-3 h-3 bg-blue-500 rounded-full mr-3 pulse/div span idstatusText classtext-gray-700等待输入.../span /div !-- 响应区 -- div idresponseArea classbg-white rounded-lg shadow-sm p-4 min-h-[200px] div idresponseContent classprose prose-sm max-w-none/div /div !-- 工具调用区 -- div idtoolArea classmt-6 space-y-3/div /div script let eventSource null; let currentResponse ; let toolCards {}; // 状态协调器处理事件更新状态 function handleEvent(event) { const data JSON.parse(event.data); switch(event.type) { case status: document.getElementById(statusText).textContent data.message; if (data.progress 100) { document.getElementById(pulseDot).className w-3 h-3 bg-green-500 rounded-full mr-3; } break; case tool_call: const toolId data.id || tool_${Date.now()}; toolCards[toolId] { element: createToolCard(data.tool, data.query, toolId), status: running }; document.getElementById(toolArea).appendChild(toolCards[toolId].element); break; case tool_result: if (toolCards[data.tool_id]) { toolCards[data.tool_id].status done; const card toolCards[data.tool_id].element; card.querySelector(.tool-progress).textContent 已完成; card.querySelector(.tool-result).textContent 已获取${data.result.length}条结果; } break; case llm_chunk: currentResponse data.token; document.getElementById(responseContent).innerHTML highlightText(currentResponse); // 滚动到底部 document.getElementById(responseArea).scrollTop document.getElementById(responseArea).scrollHeight; break; case error: alert(Agent错误: ${data.message}); closeEventSource(); break; } } // 创建工具调用卡片 function createToolCard(toolName, query, toolId) { const div document.createElement(div); div.className tool-card bg-gray-50 p-3 rounded border border-gray-200; div.innerHTML div classflex justify-between items-start div div classfont-medium text-gray-800${getToolIcon(toolName)} ${toolName}/div div classtext-sm text-gray-600 mt-1${query}/div /div button onclickcancelTool(${toolId}) classtext-xs text-red-500 hover:text-red-700取消/button /div div classmt-2 flex items-center div classtool-progress text-sm text-gray-500加载中.../div div classml-2 w-16 h-1.5 bg-gray-200 rounded-full overflow-hidden div classh-full bg-blue-500 rounded-full animate-pulse/div /div /div div classtool-result text-sm text-green-600 mt-1 hidden/div ; return div; } function getToolIcon(toolName) { const icons { search: , database: ️, api: }; return icons[toolName] || ⚙️; } // 文本高亮函数 function highlightText(text) { return text .replace(/(iPhone \d|Huawei Mate \d|Pro|Ultra)/g, span classtoken-noun$1/span) .replace(/(支持|搭载|优于|强于|弱于|适合|推荐)/g, span classtoken-verb$1/span) .replace(/(\d万|f\/\d\.\d|\dMP)/g, span classtoken-number$1/span) .replace(/(更.*?|最优|最强|最佳)/g, span classtoken-comparative$1/span); } // 取消工具调用 window.cancelTool function(toolId) { if (toolCards[toolId] toolCards[toolId].status running) { fetch(/agent/cancel?tool_id${toolId}) .then(() { toolCards[toolId].status cancelled; toolCards[toolId].element.querySelector(.tool-progress).textContent 已取消; }); } }; // 启动Agent流 document.getElementById(submitBtn).onclick function() { const query document.getElementById(queryInput).value.trim(); if (!query) return; // 关闭旧连接 closeEventSource(); // 创建新EventSource eventSource new EventSource(/agent/stream?query${encodeURIComponent(query)}); // 绑定事件 eventSource.addEventListener(status, handleEvent); eventSource.addEventListener(tool_call, handleEvent); eventSource.addEventListener(tool_result, handleEvent); eventSource.addEventListener(llm_chunk, handleEvent); eventSource.addEventListener(error, handleEvent); // 错误处理 eventSource.onerror function() { console.error(EventSource connection failed); document.getElementById(statusText).textContent 连接失败请检查网络; document.getElementById(pulseDot).className w-3 h-3 bg-red-500 rounded-full mr-3; }; }; // 关闭连接 function closeEventSource() { if (eventSource) { eventSource.close(); eventSource null; } // 清空UI currentResponse ; document.getElementById(responseContent).innerHTML ; document.getElementById(toolArea).innerHTML ; document.getElementById(statusText).textContent 等待输入...; document.getElementById(pulseDot).className w-3 h-3 bg-blue-500 rounded-full mr-3 pulse; } /script /body /html实操心得高亮逻辑要轻量我们用正则而非复杂NLP库因为highlightText()在每次llm_chunk事件中执行必须保证1ms。正则/(iPhone \d)/g比new RegExp(iPhone \\d, g)快3倍V8引擎优化。取消按钮的实现前端点击“取消”发送/agent/cancel?tool_idxxx请求服务端需有对应接口终止后台任务如asyncio.cancel(task)。这是用户掌控感的核心。滚动到底部scrollTop scrollHeight是必须的否则长响应会“消失”在视口外。但要注意频繁设置scrollTop会触发重排reflow我们用requestAnimationFrame做了节流但此处为简化省略。4.4 Nginx部署配置让SSE在生产环境稳如磐石创建nginx.confupstream agent_backend { server 127.0.0.1:8000; } server { listen 80;

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑