先把结论甩前面:别让浏览器直连大模型流式接口。中间架一层 Node 做 SSE 反向代理,顺手把鉴权和限流挡在这层,前端只管 fetch 自己后端,密钥不落地、谁能调你说了算。我上周刚因为没这层,被一个实习生用 devtools 把 key 抠出来,半天烧了三十多块 token,血的教训。
事情起因是这样。我们有个内部的写作辅助小工具,前端直接拿着模型的 stream endpoint 在跑。能用,但有两个雷:一是 key 写在前端,F12 一翻就有;二是没人拦着,谁手快谁多调,某天账单突然鼓了一截才发现。于是我把流量全收回 Node 这层。核心就是把上游的 text/event-stream 原样往下透,中间插一道鉴权和一个最朴素的令牌桶。代码大概长这样,没多少行:
javascript
const express = require('express')
const app = express()
// 极简内存令牌桶,够内部用了
const buckets = new Map()
function rateLimit(uid, cap = 20, refill = 1) {
const now = Date.now()
const b = buckets.get(uid) || { tokens: cap, ts: now }
b.tokens = Math.min(cap, b.tokens + (now - b.ts) / 1000 * refill)
b.ts = now
buckets.set(uid, b)
if (b.tokens < 1) return false
b.tokens -= 1
return true
}
app.post('/api/chat', async (req, res) => {
const uid = verifyToken(req.headers.authorization) // 自己的鉴权,过不了直接 401
if (!uid) return res.status(401).end()
if (!rateLimit(uid)) return res.status(429).end('慢点儿')
const upstream = await fetch(UPSTREAM_URL, {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.MODEL_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify(req.body),
})
// SSE 透传的关键:头要对,别被 nginx/代理缓冲住
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('X-Accel-Buffering', 'no')
res.flushHeaders()
for await (const chunk of upstream.body) {
res.write(chunk) // 上游 data: 行原样往下倒
}
res.end()
})
踩过的坑提两个。X-Accel-Buffering: no 这行别漏,我们线上挂了 nginx,不加它流式会被攒成一坨一次性吐出来,前端那个逐字蹦的效果全没了,体验直接退化成转圈等。还有客户端断开要监听 req.on('close') 去 abort 上游,不然用户关了页面你这边还在烧 token------上面那段为了短我省了,真上线得补。学习曲线不算陡,但 SSE 透传这块第一次弄总会卡在缓冲和断连上,大半天就耗这儿了。
代理这层稳了之后我有点上头,想着前端那个写作工具的"脑子"能不能也别自己硬编。正好试了个零代码搭智能体的东西,拖一拖配一配,挂个现成模型、把我们团队的文案规范喂成知识库,十几分钟就出了个能用的小助手,发布成一个 API 接口。我那个 Node 代理后面接的上游,从直连模型换成了这玩意儿给的 endpoint,前端代码一行没动。说实话当时有点惊到------我对它说"按我们这套 tone 重写这段",它真就照知识库里的规矩改了,不是泛泛的客气话。缺点也有,第一版回答太干、太"正确",得反复调提示词和知识库切片才有人味儿;而且它只干内容生成这种杂活,工程的脏活累活还是 Node 这层自己扛。但能让我这种懒人不写后端逻辑就攒出个有业务知识的智能体,挺香。(模型/API 我走的讯飞 MaaS,现成调,没自己部署算力。)
绕回来说一句:鉴权限流这种事,放前端是侥幸,放 Node 这层才睡得着。你们的大模型流式接口是直连还是也加了代理层?评论区聊聊各自的令牌桶都用啥扛的。