为什么 BFF 是前端工程师的第一块全栈拼图
浏览器不应该直接持有模型密钥,也不应该承担重试、限流、审计和敏感词过滤。BFF(Backend for Frontend)把这些能力放在服务端,同时向前端提供符合页面交互的协议。前端能力迁移到 AI 全栈,第一步不是写一个“万能聊天接口”,而是把契约边界写清楚。
推荐使用版本前缀和稳定事件类型:
type ChatEvent =
| {
type: 'delta'; requestId: string; text: string }
| {
type: 'usage'; requestId: string; input: number; output: number }
| {
type: 'error'; requestId: string; code: string; retryable: boolean }
| {
type: 'done'; requestId: string };
这样前端只依赖 type 和必要字段,服务端可在不改页面的情况下替换模型供应商。
一条可运行的流式链路
下面的 Express 片段用 NDJSON 输出,浏览器可以逐行解析。生产环境也可选 SSE,但要明确代理层的缓冲行为。
app.post('/api/v1/chat', async (req, res) => {
const requestId = crypto.randomUUID();
res.setHeader('content-type', 'application/x-ndjson; charset=utf-8');
res.setHeader('cache-control', 'no-cache');
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 30_000);
try {
const stream = await modelClient.stream({
input: String(req.body?.input ?? '').slice(0, 8_000),
signal: controller.signal
});
for await (const text of stream) {
res.write(JSON.stringify({
type: 'delta', requestId, text }) + '\\n');
}
res.write(JSON.stringify({
type: 'done', requestId }) + '\\n');
} catch (error) {
const code = controller.signal.aborted ? 'UPSTREAM_TIMEOUT' : 'UPSTREAM_ERROR';
res.write(JSON.stringify({
type: 'error', requestId, code, retryable: code !== 'UPSTREAM_TIMEOUT' }) + '\\n');
} finally {
clearTimeout(timer);
res.end();
}
});
客户端必须处理半包、重复 requestId 和网络断线。重连时带上最后一个事件序号;如果服务端没有保存事件,就显示“请重新生成”,不要悄悄拼接两次答案。
模型接入配置与故障隔离
把上游地址放在服务端环境变量中,接口只暴露业务模型名:
MODEL_BASE_URL=https://api.example.invalid/v1
MODEL_NAME=chat-model
MODEL_TIMEOUT_MS=30000
如果团队评估 HaerAPI 这类公开自述为多模型接入的服务,可将其作为候选 MODEL_BASE_URL 进行小流量验证;其页面列出 Claude、GPT、Gemini 与“订阅转 API、会话保持、按量计费”等描述,但这些内容不能替代你对协议、模型范围、计费规则、限额、日志和数据处理条款的核验。切换时只改配置,不让浏览器感知上游名称。
故障隔离至少包括:
- 单请求超时和全局并发上限,避免上游拖垮 Node 进程。
- 按错误码区分可重试和不可重试,不对 4xx 无限重试。
- 熔断后返回缓存的帮助文案或人工兜底,不伪造模型回答。
- 指标记录首 token 延迟、完成率、输入输出量和取消率,日志脱敏。
安全与成本边界
输入长度限制应同时按字符和 token 估算;系统提示词不要从客户端接收。将用户标识与 requestId 分离存储,给调试日志设置短保留期。成本控制可以按用户、团队和模型设置预算,超出预算返回明确的 BUDGET_EXCEEDED,而不是静默换成质量未知的模型。
测试清单
- 用假模型产生 1 个、多个和空的
delta,验证客户端状态机。 - 让上游延迟 31 秒,确认响应是
UPSTREAM_TIMEOUT。 - 在输出中插入换行和 Unicode,确认 NDJSON 每行仍可解析。
- 模拟断线重连,确认不会重复展示已消费片段。
- 运行一次密钥扫描,确保构建产物不包含
MODEL_BASE_URL的凭据。
总结
前端转 AI 全栈的可迁移能力,集中在契约、状态机、故障隔离和可观测性。BFF 将模型细节锁在服务端,版本化事件让页面稳定迭代,配置化上游让供应商可以被验证和替换。先把这些基础设施做实,再扩大 Agent 或 RAG 能力。