当大模型遇见BFF,流式输出的体验提升不止一点点
写在前面
最近在做一个AI对话相关的项目,涉及到流式输出的场景。前端需要调用大模型API,但直接在前端调用会遇到两个问题:一是API Key暴露的安全风险,二是跨域问题。这篇文章就记录一下我是如何通过BFF层来解决这些问题的。
什么是BFF?
BFF(Backend For Frontend),直译过来就是"为前端服务的后端"。这个概念在微服务架构中很常见,但在大前端领域同样适用。
传统模式下,前端直接调用后端Java/Go服务提供的RESTful接口,实现数据的增删改查。但随着前端业务越来越复杂,特别是涉及到流式输出、二进制数据处理等场景时,直接让前端处理这些逻辑会让代码变得臃肿且难以维护。
于是我们可以在前端和后端之间加一层Node服务:
text
前端(Vue/React) → Node(BFF) → 后端(Java/LLM)
这一层Node服务可以帮我们做很多事情:
- 聚合多个接口的数据
- 处理流式输出的解析逻辑
- 隐藏敏感的API Key
- 解决跨域问题
SSE(Server-Sent Events)是什么?
SSE是一种基于HTTP的服务器推送技术,允许服务器向客户端推送事件流。与WebSocket不同,SSE是单向的,只能由服务器向客户端发送数据。
在AI对话场景中,大模型通常以流式方式返回结果,SSE正是处理这种场景的理想方案。
项目结构一览
text
bash
stream-bff/
├── src/
│ ├── App.vue # 前端主组件
│ └── main.js # Vue入口
├── server.mjs # BFF后端服务(Express)
├── vite.config.js # Vite配置(含代理)
├── .env.local # 环境变量(API Key)
├── package.json # 项目依赖
└── readme67.md # 笔记文档
环境配置
1. 安装依赖
bash
csharp
pnpm add express dotenv
2. 环境变量管理
在.env.local中存放敏感信息:
env
ini
VITE_DEEPSEEK_API_KEY=your_api_key_here
注意 :.env.local应该添加到.gitignore中,避免API Key泄露。
3. Vite代理配置
跨域问题的本质是浏览器的同源策略:只要域名、端口、协议任意一个不同,就会触发跨域限制。
vite.config.js中配置代理来解决跨域:
javascript
javascript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
secure: false,
rewrite: (path) => path.replace(/^/api/, '')
}
}
}
})
这样,前端请求/api/stream会被Vite代理转发到http://localhost:3000/stream,完美绕过了跨域问题。
BFF层实现
Express服务搭建
server.mjs是BFF层的核心:
javascript
javascript
import * as dotenv from 'dotenv';
import express from 'express';
dotenv.config({
path: ['.env.local', '.env']
});
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.send('hello world');
});
// 流式输出接口
app.get('/stream', async (req, res) => {
const { prompt } = req.query;
const endpoint = 'https://api.deepseek.com/v1/chat/completions';
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.VITE_DEEPSEEK_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'deepseek-v4-flash',
stream: true,
messages: [{ role: 'user', content: prompt }]
})
});
// response.body 是 ReadableStream
// 这里可以进行流式数据的处理和转发
console.log(response.body);
} catch (err) {
console.log(err);
}
});
app.listen(port, () => {
console.log(`服务器在${port}端口启动了`);
});
请求链路
text
bash
前端:5173/api/stream
→ Vite代理转发
→ BFF:3000/stream
→ DeepSeek API
前端实现
App.vue中的核心逻辑:
vue
xml
<script setup>
import { ref } from 'vue';
const question = ref('');
const content = ref('');
const stream = ref(true);
const update = async () => {
// 发送请求到BFF层
fetch(`/api/stream?prompt=${question.value}`)
.then(res => res.json())
.then(data => {
console.log(data);
});
}
</script>
<template>
<div class="container">
<div>
<label>输入:</label>
<input class="input" v-model="question" />
<button @click="update">提交</button>
</div>
<div class="output">
<div>
<label>Streaming</label>
<input type="checkbox" v-model="stream" />
</div>
<div>{{ content }}</div>
</div>
</div>
</template>
<style>
.container {
display: flex;
flex-direction: column;
align-items: start;
justify-content: start;
height: 100vh;
font-size: 0.85rem;
}
.input {
width: 200px;
}
.output {
margin-top: 10px;
min-height: 300px;
width: 100%;
text-align: left;
}
button {
padding: 0 10px;
margin-left: 6px;
}
</style>
启动服务
项目需要同时启动两个服务:
bash
bash
# 终端1:启动Vite开发服务器(前端)
npm run dev
# 终端2:启动BFF服务(后端)
node server.mjs
踩坑记录
关于502错误
配置代理后如果遇到502,通常是因为:
- BFF服务没有启动(确认端口3000是否在监听)
- 代理的目标地址配置错误
- 目标服务返回了非预期的响应
关于API Key安全
前端代码中的环境变量只有在构建时才会被替换,如果直接在前端调用大模型API,用户可以通过查看源代码获取到API Key。通过BFF层转发,API Key只存在于服务端,安全性大大提高。
总结
通过BFF层的引入,我们实现了:
- 解决跨域问题 - Vite代理将请求转发到同源的BFF服务
- 隐藏敏感信息 - API Key存储在服务端,不会暴露给前端
- 简化前端逻辑 - 复杂的流式数据处理交给BFF层处理
- 架构更清晰 - 前端专注于UI交互,BFF层负责数据适配和转发
这种模式在微前端、大前端架构中非常实用,特别是当我们需要对接多个后端服务或第三方API时,BFF层的价值会更加明显。