资讯动态

从SSE到AG-UI:构建生产级AI流式输出架构

发布时间:2026/10/26 21:53:41 来源:尧图企业网站定制
最近在梳理生成式AI应用的工程链路时我发现自己和团队花了整整一个迭代去处理同一个问题模型输出的token怎么才能又快又稳地到达用户屏幕同时还能在执行工具调用时把结果同步给前端。这个问题看着简单但一旦涉及Agent、MCP、长任务写入传统的HTTP请求-响应模型就撑不住了。于是我们顺着SSE走到了AG-UI。这篇文章会从SSE的边界讲起说清楚AG-UI解决了哪些真实痛点然后给出一套可以直接参考的生产级流式输出架构包括MCP工具流式输出内容到文件的落地细节希望给正在做AI应用前后端、搭建Agent平台或者准备把AI能力接入业务系统的同学一些可复用的经验。1. 为什么要重新设计流式输出SSE时代的边界与局限1.1 从轮询到长连接SSE如何成为AI流式输出的第一站聊AG-UI之前得先回到SSE。SSE的全称是Server-Sent Events中文常叫“服务端单向推送”。它的工作方式很简单浏览器通过EventSource发起一个HTTP长连接服务端用text/event-stream格式持续返回事件客户端一直接收直到连接关闭。相比WebSocket它最大的优势是基于HTTP天然支持自动重连、消息ID追认和缓存语义不需要额外握手协议部署和调试都很轻。这正好贴合大模型应用初期的诉求。模型输出是单向、松散的文本流客户端不需要频繁上行消息只需要不断接收增量。于是SSE几乎成了AI流式输出的默认起点。早期实现思路千篇一律后端调模型SDK拿到流式chunk拼成data: {delta:hello}\n\n发出去前端用fetch reader边读边渲染。这个阶段能跑通但离“架构设计”还有距离。第一个暴露的问题是消息格式没有统一标准。SSE协议本身只定义了data字段里面塞什么完全由业务自己说了算。团队A用data: {type:token,value:...}团队B用data: {code:200,data:{text:...}}前端为了适配不同后端只能各自写一套“暗号解析器”。一旦Agent系统开始复杂整个链路就会变成拼维护成本。1.2 原生SSE在Agent场景中的三个硬伤随着AI应用从纯聊天走向Agent化SSE的不足变得非常具体我归纳成三个硬伤。第一个是事件语义太弱。Agent场景下流里不仅要传文本还要传工具调用参数、工具执行结果、进度信息、错误信息、引用来源。SSE只给了一个data字段增量文本和工具调用参数混在一起前端必须自己拼装、判断边界。举个例子一个write_file工具的参数往往是流式到达的JSON片段{path:/tmp/a.txt,content:开头几个字...}可能分成十几次才发完。前端要维护一个缓冲区一边等参数完整一边决定何时触发工具调用渲染。这个逻辑写在业务代码里只会越滚越复杂。第二个是断线重连的恢复成本高。EventSource确实提供了Last-Event-ID重连机制但服务端是否记录消费位置、是否支持从中间恢复完全取决于业务实现。模型流式响应通常是一次性管道断点之后没法接着生成。更麻烦的是工具已经执行了一半客户端断线重连后不知道该不该重新执行很容易出现重复写文件或者漏掉结果。第三个是聊天框之外的场景表达不出来。AI Native应用不只是聊天框自动化工作流、编辑器补全、Agent调用MCP工具写文件、多Agent协作每一步都是一个事件。SSE本质是单向文本推送没有状态、没有能力协商、没有消息类型约束。越往后做越能感觉到“给协议打补丁”不如“引入面向AI交互的协议”。2. AG-UI到底是什么面向AI Agent的统一消息协议2.1 AG-UI的核心消息模型增量、工具、状态一次说清AG-UI全称Agent User Interface是一套面向“AI Agent与用户界面之间通信”的开放消息协议近年在Vercel AI SDK生态里被大量讨论和落地。它的核心思路是把一次模型交互拆成多种语义明确的消息类型包括文本增量、工具输入增量、工具结果、推理过程、完成标记等。消息本身用JSON表示底层可以用JSON Lines、SSE或WebSocket承载重点是“事件契约”统一了。你可以把AG-UI理解成“前端UI能直接消费的数据契约”。服务端不再发一个自造的JSON而是发送协议约定的事件前端也不需要针对每家模型服务商写解析逻辑。协议里几个关键消息类型非常贴近AI场景文本增量text-delta模型新生成的一段文本前端直接追加显示。工具输入增量tool-input-delta工具调用的参数以流式片段到达前端可以边收边渲染。工具结果tool-result工具执行完成后返回的结构化结果。推理过程reasoning模型思考链或摘要适合做过程展示。完成标记finish本次交互结束前端可以收尾。这种拆法最大的价值在于“模型输出与UI更新的形状对齐”。SSE只定义了dataAG-UI则定义了一套可组合的消息类型。前端拿到text-delta就追加文本拿到tool-result就渲染工具卡片逻辑清晰也不会因为模型供应商切换而崩掉。2.2 相比SSEAG-UI解决了什么关键问题对比SSEAG-UI的改进不在传输层而在消息契约层。它带来三个直接收益。第一前端渲染与后端模型解耦。今天接GPT明天换Claude后天接本地模型只要后端把模型输出规范成AG-UI消息前端代码几乎不用动。对团队来说这意味着模型选型切换的边际成本大大降低。第二工具调用成为一等公民。在SSE方案里工具调用往往被塞进自定义字段前端要自己判断“这是文本还是工具参数”。AG-UI把工具调用拆成增量事件拼装逻辑可以前置到网关或SDK里前端只需要按协议处理。这正好衔接MCP工具生态Agent可以调用写文件、查数据库、发HTTP请求等工具而每一步都通过标准消息推给前端。第三支持多模态与富UI。文本、图片、音频、文件输出都能以标准消息形态出现。对MCP工具来说写文件时可以把进度实时推给用户体验从“黑盒等待”变成“可视化执行”。需要提醒的是AG-UI目前仍在快速演进不同实现之间字段存在细微差异。生产落地时建议锁定协议版本并在网关层做字段适配避免上下游各自升级导致消息不兼容。3. 生产级AI Native流式输出架构的分层设计与关键环节3.1 分层架构接入层、编排层、模型网关与工具层重新设计AI Native流式输出架构时我习惯把它拆成四层每一层职责单一避免模型和前端直接耦合。接入层面向浏览器和客户端负责序列化与连接生命周期管理。它可以是SSE、WebSocket或AG-UI JSONL核心任务是把内部事件按客户端协议输出并处理心跳、断线、取消等底层问题。编排层接收上游消息维护会话状态执行思维链和规划。它只处理业务语义不关心底层是哪个模型。模型网关统一封装各家模型API负责模型调用、超时、重试和流式增量规范化。工具层就是MCP Server/Client、HTTP工具、内部RPC负责执行真实动作比如写文件、查数据库。AI Native不是把模型API直接暴露给前端而是围绕这四层提供可观测、可治理的通道。这里最容易被低估的是模型网关。不同模型对工具调用的流式格式差异非常大有的模型先发工具名称再发参数有的直接发完整的arguments JSON有的会用多个增量片段拼成一个JSON。网关如果不做缓冲区归一化下游AG-UI消息就没法稳定。这块我在项目里花的时间最多但也是整个架构里性价比最高的部分。3.2 增量缓冲与背压控制让流式输出可回放、可限速流式架构里第二个常见问题是“太快”。模型输出快并不代表客户端消化得了也不代表下游写文件跟得上。这里需要两个配套组件增量缓冲区和背压控制器。增量缓冲区把模型发来的chunk暂存再按语义完整性拆分或合并。比如工具调用参数没到齐时先把片段存在缓冲区等到可以拼成一个合法JSON后再发tool-input-delta。这能避免前端拿到一堆半截参数却没法处理。背压控制可以理解成一个阀门当消费者处理速度跟不上时服务端主动降速或改为低频批量发送防止连接堆积。实现上我会在网关层放一个有界队列队列水位超过阈值就暂停拉取模型流等下游消费后再继续。与其无脑转发不如让每一步都有明确消费进度。另一个容易被忽视的设计是“可回放”。不是所有场景都需要但生产环境建议至少保留最近N条已发送消息供客户端断线重连后快速补偿。这和SSE的Last-Event-ID思路一脉相承只是到AG-UI下消息类型更多补偿时需要“会话快照增量事件”一起处理。3.3 工具调用的流式写入MCP工具输出内容到文件的落地方案这是最近被问得很多的一个场景特别是Cherry Studio这类客户端支持自定义MCP服务后“用MCP工具流式输出内容到文件”成了高频需求。简单说就是Agent在生成内容的同时调用一个write_file之类的MCP工具把流式内容分块写入目标文件并把进度反馈给前端。完整链路一般长这样Agent规划后产生一个write_file工具调用参数里包含目标路径和内容来源。服务端先发送tool-input-delta前端立刻显示“准备写入文件”。工具层开始接收模型文本流按固定块大小追加写入文件。每写完一块通过MCP协议返回进度服务端把进度包装成AG-UI消息发给前端。全部写入完成工具返回包含文件路径、大小、最终状态的tool-result。实操中有两个容易翻车的点。第一是幂等性如果网络中断、客户端重连后重复请求工具会不会重复写文件我习惯给每次工具调用生成唯一runId写文件前先检查目标文件是否存在同名锁文件存在就拒绝或续写。第二是并发控制多个Agent实例同时写同一个路径必须避免互相覆盖。可以引入文件锁或者在路径里注入会话ID。配置MCP Server时超时参数也要调大长文件写入不可能秒级返回。3.4 超时、重连与幂等生产环境的稳定性保障流式接口和普通接口不一样不能用“请求超时5秒”来管理。一个Agent可能规划10秒、调工具20秒、写文件30秒但用户看到的内容一直是连续推进的。所以超时要拆成“空闲超时”和“总时限”两类。空闲超时指超过多少秒没有任何事件返回就判定链路卡死总时限是整个请求允许的最大时长达到后服务端发标准错误消息并关闭流。这两类超时最好分别配置不能混为一谈。客户端重连时我建议带上会话ID和最后一条消费序号服务端根据序号补偿漏掉的事件。如果序号已经过期可以降级为“重建会话”并清空前端旧状态避免渲染错乱。所有写入类工具调用入参里一定要加requestId服务端记录执行状态重放前先查询状态防止同一个文件被写两遍。4. 实操从SSE升级到AG-UI的落地步骤与核心代码4.1 服务端实现AG-UI的JSONL流式响应这里用Node.js的Express写一个最小编程示例展示AG-UI服务端应该如何组织响应。核心思路是每一行写一个完整JSON对象客户端按行解析。app.post(/api/chat, async (req, res) { // 使用JSON Lines格式每行一个JSON对象 res.setHeader(Content-Type, application/x-ndjson); res.setHeader(Cache-Control, no-cache); res.setHeader(Connection, keep-alive); res.flushHeaders?.(); const sessionId crypto.randomUUID(); res.write(JSON.stringify({ type: session, sessionId }) \n); const stream await model.stream({ messages: req.body.messages, tools: [{ name: write_file, description: 写入文件 }], }); for await (const chunk of stream) { if (chunk.type text) { // 把模型增量转成AG-UI文本增量消息 res.write(JSON.stringify({ type: text-delta, delta: chunk.text }) \n); } if (chunk.type tool_call) { const parsed normalizeToolCall(chunk); if (parsed.ready) { res.write(JSON.stringify({ type: tool-input-delta, toolCallId: parsed.id, delta: parsed.argsText }) \n); } } } res.write(JSON.stringify({ type: finish, sessionId }) \n); res.end(); });这段代码展示了从模型chunk到AG-UI消息的基本转换。实际工程里还要处理工具执行循环、错误事件和取消信号建议把消息序列化封装成独立模块别在业务回调里拼JSON后续维护会轻松很多。4.2 前端如何消费和渲染流式增量前端侧fetch本身可以读取流式响应。通过ReadableStream不断按行拆包根据消息类型分发。核心逻辑不复杂但要注意渲染方式。const resp await fetch(/api/chat, { method: POST, body: JSON.stringify({ messages }), }); const reader resp.body.getReader(); const decoder new TextDecoder(); let buffer ; while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); let idx; while ((idx buffer.indexOf(\n)) 0) { const line buffer.slice(0, idx).trim(); buffer buffer.slice(idx 1); if (!line) continue; const msg JSON.parse(line); switch (msg.type) { case text-delta: appendText(msg.delta); break; case tool-input-delta: showToolProgress(msg); break; case tool-result: renderToolResult(msg); break; case finish: finalize(); break; } } }这里的关键是“增量渲染而不是全量重绘”。文本增量直接追加到当前段落工具状态放在一个Map里按toolCallId更新对应卡片。这样能避免大段内容频繁重建DOM用户滑动时也不会被重置位置。4.3 与MCP工具联动流式输出内容到文件的完整示例需要强调下面这段是框架无关的伪代码但覆盖了“流式读取内容分块写文件进度上抛工具结果返回”四个核心环节。async function runWriteFileTool(toolCallId, params, onProgress) { const targetPath params.path; const stream createContentStream(params.contentFrom); let offset 0; // 先获取文件锁防止并发覆盖 await acquireFileLock(targetPath); for await (const chunk of stream) { await appendFile(targetPath, chunk); offset chunk.length; onProgress({ type: tool-event, toolCallId, stage: writing, bytesWritten: offset, }); } releaseFileLock(targetPath); return { type: tool-result, toolCallId, result: { path: targetPath, size: offset, ok: true }, }; }onProgress回调里把进度转成AG-UI消息发到前端用户就能看到“正在写入XX字节”的实时进度。需要特别注意的是appendFile要保证原子性和序列化不能并发写同一个文件。如果配置的MCP Server只返回普通JSON而不是流式也没关系网关可以把一次返回拆成“开始、进行中、完成”三条协议消息照样能模拟流式体验。4.4 新旧协议共存时的兼容策略生产系统不太可能一夜之间全切AG-UI。比较稳妥的做法是在网关层做协议适配内部统一用AG-UI风格的事件对象对外按客户端能力分别输出SSE或AG-UI JSONL。判断逻辑可以很简单客户端请求头带Accept: application/x-ndjson就返回AG-UI否则退回SSE。这样升级成本被控制在网关一个模块旧客户端不需要改。迁移期间比较容易踩的坑是维护两套协议后SSE补全逻辑和AG-UI补全逻辑不一致。解决办法是让两套协议共用同一个“事件源”只在序列化层分流绝不在业务层分流。同时协议适配层一定要做监控埋点记录消息类型、数量、耗时和断线次数这些数据对后续优化特别有用。5. 常见问题与排查实录5.1 断线重连后事件丢失如何做可靠性兜底现象很典型用户弱网环境下前端EventSource重连后工具结果丢了页面一直转圈。排查后发现服务端没有保存Last-Event-ID对应的缓冲区重连后直接从新请求开始之前的事件自然找不到。解决思路是给每个会话分配连续序号客户端断线带着最后消费序号服务端从Redis里拉取该序号之后的事件补发。缓冲区可以设置过期时间比如5分钟超时返回session_expired让前端重建会话并提示“历史内容已失效”。这里的关键不是依赖某个协议自带的重连而是把“可恢复”当成一等需求提前设计。5.2 工具结果与文本增量乱序如何处理时序另一个高频问题是乱序Agent先调用工具但文本增量还在继续发前端一会儿显示文本一会儿显示工具结果卡片顺序错乱。根因通常是模型输出本身多路并发或者网关在转发时存在异步竞争。我的解决方法是引入“时间线水位线”每条消息带递增序号前端收到后先放入待渲染队列等序号连续时再统一提交渲染。实际不需要等所有消息到齐只要保证每个消息类型的局部顺序稳定即可。更简单的做法是按工具调用ID分组同一工具的增量按时间线性展示不同工具之间允许乱序。这两种方案都能有效减少渲染错乱。5.3 高并发下SSE连接内存暴涨怎样做资源治理压测时遇到过一个典型问题几千个SSE连接直接把Node进程内存打满。分析后发现三个原因第一每个连接都持有一个模型流引用模型端没有及时关闭第二没有设置连接空闲超时僵尸连接一直挂着第三数据发送速度比客户端消费速度快服务端写缓冲不断堆积。对应策略是三件事在网关层加有界队列和背压队列满了就暂停拉取模型流给所有连接设置心跳和空闲超时客户端断线时立刻通过AbortController取消模型流做级联取消。这套组合下来内存曲线变得平滑很多。5.4 监控指标与压测经验流式架构上线前必查清单最后分享一份上线前必查的指标清单首字节时延TTFB从请求发起到客户端收到第一个消息的时间最好小于500ms。平均token间隔模型流中相邻token的到达间隔用来判断模型网关是否成了瓶颈。事件积压数有界队列当前堆积的消息数量接近阈值要告警。重连率单位时间断线重连次数突增通常意味着网关或网络异常。工具成功率与工具耗时写文件类工具尤其盯P99耗时。压测别只测“并发连接数”要测“流持续30秒以上的并发”。一秒钟就结束的请求和多轮工具调用持续1分钟的请求资源占用完全不是一个量级。用真实Agent场景去压测数据才有参考价值。上线初期我习惯给流式接口设置全局最大并发数超出直接排队避免雪崩。我个人在实践中最深的体感是协议选择只是水面上的部分真正的工程量在事件契约、缓冲、背压和可恢复性。AG-UI让我少写了很多“前端对暗号”的解析代码但它不会自动让架构变稳。如果你正打算从SSE升级建议先把内部消息模型定义清楚再决定传输层用哪套协议如果你在做MCP工具服务优先把文件写入的幂等和进度上报做好比纠结用什么事件格式更重要。后续如果再扩展我会把多模态事件和本地缓存补偿也纳入这套架构持续补课。

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

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

免费获取报价 →
↑