为什么大模型回答总是一个字一个字蹦出来?聊聊 SSE 流式输出与 BFF 层的那些事

你有没有好奇过,ChatGPT、DeepSeek 这些大模型的回答,为什么不是一次性返回,而是一个字一个字地"蹦"出来?这背后藏着前端、后端、甚至一个"中间层"的精妙协作。今天我们就来拆解它。

一、先抛问题:流式输出到底难在哪?

如果你写过一个普通的接口请求,大概是这样:

js 复制代码
fetch('/api/data').then(res => res.json()).then(data => console.log(data))

一次性返回,简单粗暴。但大模型的回答动辄几百上千字,等它全部生成完再返回,用户早就跑了。

于是有了流式输出(Streaming):模型生成一个 token,就推一个 token 给前端,边生成边显示。

可问题来了------前端处理流式输出并不简单:

  • 二进制流对象(ReadableStream)要解码
  • data: 字段要解析
  • 各种异常、断流要处理

前端业务已经够复杂了,能不能把这些脏活累活剥离出去?

二、BFF 层:前端的"私人助理"

2.1 什么是 BFF?

复制代码
前端(Vue/React)  ------>  BFF(Node)  ------>  Backend(Java/Go)

BFF = Backend For Frontend,为前端服务的后端。

它不是纯后端,也不是纯前端,而是夹在中间的一层 Node 服务,专门替前端"挡子弹":

  • 接口改字段?BFF 改一下就行,不用等后端排期
  • 流式解码复杂?BFF 处理完,吐给前端干净的数据
  • API Key 不想暴露?BFF 帮你藏起来

2.2 为什么 BFF 特别适合"大前端工程师"?

传统分工里,前端写 Vue/React,后端写 Java/Go。但现实是------前端有太多细碎的需求(改个字段、聚合几个接口、做个流式解析),等后端排期太慢。

Node 的出现让前端工程师能自己写轻量后端:一个 server.mjs 文件就是一个服务。于是"大前端"的概念诞生了------前端 + BFF,一个人全包。

三、Node 框架开发:三行代码起一个服务

BFF 层用 Node 怎么写?最经典的就是 Express:

js 复制代码
import express from 'express';

const app = express();        // 实例化一个 server app
const port = 3000;

app.get('/', (req, res) => {
  res.send('Hello World!');   // 一次性发送
});

app.listen(port, () => {
  console.log(`服务器在${port}端口启动了`);
});

三步走:引入框架 → 实例化 app → 监听端口。就这么简单,一个 HTTP 服务就起来了。

四、流式输出的 BFF 封装

前端不想碰 ReadableStream 的解码细节?那就让 BFF 来。前端只需要:

js 复制代码
fetch('/api/stream?prompt=hello')
  .then(res => res.json())
  .then(data => console.log(data));

干净、清爽。复杂的流式逻辑全在 BFF 层的 /stream 路由里:

js 复制代码
app.get('/stream', async (req, res) => {
  const { prompt } = req.query;
  const response = await fetch(endpoint, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.VITE_DEEPSEEK_API_KEY}`
    },
    body: JSON.stringify({
      model: 'deepseek-v4-flash',
      stream: true,
      messages: [{ role: 'user', content: prompt }]
    })
  });
  // response.body 是一个 ReadableStream
  // 在 BFF 层解码、解析 data: 字段、再吐给前端
});

为什么要放在 BFF 层做?

原因 说明
降低前端复杂度 二进制流解码、data: 解析全在 BFF 完成,前端只管拿干净数据
API Key 安全 纯前端放 key,右键看源码就泄露;放 BFF,前端永远看不到
职责清晰 前端专注 UI,BFF 专注数据加工,各司其职

五、跨域问题:同源策略挡了路

BFF 写好了,前端 fetch('/api/stream') 一请求------跨域报错。

5.1 什么是跨域?

只要域名、端口、协议 有一个不同,浏览器就会触发同源策略保护机制,拦截请求。

  • 前端 Vite:http://localhost:5173
  • BFF Express:http://localhost:3000

端口不同 → 跨域。

5.2 Vite Proxy:让请求"假装"同源

既然跨域是浏览器机制,那让请求不出浏览器 不就行了?Vite 的 proxy 配置就是这个思路:

js 复制代码
// vite.config.js
export default defineConfig({
  plugins: [vue()],
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',  // 转发到 BFF
        secure: false,
        rewrite: path => path.replace(/^\/api/, '')  // /api/stream → /stream
      }
    }
  }
});

请求流程变成这样:

bash 复制代码
前端 fetch('/api/stream')
       ↓
Vite 拦截(同源,5173,不跨域)
       ↓
proxy 去掉 /api 前缀,转发到 :3000/stream
       ↓
BFF 的 /stream 路由处理

浏览器看到的始终是 5173,同源,不触发跨域保护。 Vite 在背后悄悄帮你把请求转发到了 3000 端口。

六、完整请求链路一览

javascript 复制代码
┌──────────┐   fetch('/api/stream')   ┌──────────┐   fetch + stream   ┌──────────┐
│  Vue 前端 │ ──────────────────────▶ │ Vite Proxy│ ─────────────────▶ │   BFF    │ ────────▶ DeepSeek
│ :5173    │   (同源,不跨域)         │ (5173 内) │  (:3000/stream)    │ Express  │
└──────────┘                          └──────────┘                    └──────────┘
       ▲                                                                    │
       │              BFF 解码 ReadableStream,吐干净 JSON                     │
       └──────────────────────────────────────────────────────────────────────┘

三层职责清晰:

  1. 前端:只管发请求、渲染 UI
  2. Vite Proxy:解决跨域,转发请求
  3. BFF:藏 Key、调 LLM、解码流、返回干净数据

七、关键概念速查表

概念 一句话理解
SSE Server Sent Event,服务器单向推送事件,流式输出的底层机制
BFF Backend For Frontend,替前端挡子弹的 Node 中间层
流式输出 边生成边返回,用户不用等全部生成完
同源策略 浏览器安全机制,域名/端口/协议任一不同即拦截
Vite Proxy 让请求"假装同源",在背后转发到真实后端
ReadableStream 二进制流对象,需要解码后才能读取

八、总结

大模型的流式输出,表面上是"一个字一个字蹦",背后却是一条精心设计的链路:

  • 流式输出解决"用户等待"问题
  • BFF 层解决"前端复杂度"问题
  • Vite Proxy解决"跨域"问题
  • 环境变量解决"API Key 安全"问题

每一层都在替下一层挡麻烦,这就是工程化的魅力。

相关推荐
特创数字科技1 小时前
一个纯本地运行的图片处理工具:压缩 / 裁剪 / 九宫格 / 圆角 / 滤镜 / 拼图 / 水印,终生使用
前端
IT_陈寒2 小时前
Vue的v-if和v-for混用居然是个天坑
前端·人工智能·后端
天衍四九-2 小时前
【无标题】
前端·spring boot·mysql·nginx·docker
广州华水科技2 小时前
2026年单北斗GNSS变形监测系统推荐榜单,解锁GNSS位移监测新高度
前端
xcyxiner2 小时前
DicomViewer24 修复编译bug(window test 失败)
前端·qt
linux_cfan3 小时前
videojs v10 源代码系列解读:34 · `createComposition`:类型安全的冲突检测
前端·安全
utmhikari4 小时前
【AI原生】借助AI之力优化个人博客前端样式
前端·css·hexo·next·个人博客·ai-native·vibe coding
小凯在掘金4 小时前
为什么箭头函数不能作为构造函数?
前端·javascript
JustHappy5 小时前
「vConsole MCP🛠️」我让 AI 直接看见任何 H5 的日志和请求帮你 debug
前端·javascript·程序员