前言
在之前迭代的备忘录项目中,我们已经实现AI摘要、AI润色功能。但早期方案是一次性返回全部AI结果。
当笔记内容较长时,用户点击按钮后页面会空白等待,分不清是网络卡顿还是AI正在计算,产品体验很差。
本次基于Bmob后端云进行改造,使用SSE(Server-Sent Events)服务端单向流式推送,实现类似ChatGPT的打字机逐字输出效果。
核心约束不变:
- 调用AI前先校验笔记归属,依托Bmob ACL实现数据隔离,仅可操作自己创建笔记
- 流式输出完成后新建笔记入库,不修改、不覆盖原始笔记
- 网络中断、接口报错时,禁止将不完整脏数据写入Bmob
一、整体业务流程

前端触发请求 → 后端ACL权限校验 → 校验通过建立SSE长连接
→ 请求大模型流式接口 → 后端接收文本分片,实时推送给前端
→ 前端逐字渲染文字(打字机效果)
→ 全部内容接收完毕 → 后端汇总完整文本,新建笔记存入Bmob
→ 异常捕获:断连、超时、模型报错,终止流程并提示用户
二、后端Python核心代码(SSE流式接口)
python
import requests
from flask import Flask, Response, stream_with_context, request
app = Flask(__name__)
# Bmob SDK 笔记权限校验函数
def get_note_with_auth(note_id, current_user):
note = BmobQuery("Note").get_by_id(note_id)
if not note:
return {"ok": False, "msg": "笔记不存在"}
if note["user"]["objectId"] != current_user["objectId"]:
return {"ok": False, "msg": "无权限访问该笔记"}
return {"ok": True, "data": note}
@app.route("/api/ai-stream", methods=["POST"])
def ai_stream():
req_data = request.get_json()
note_id = req_data.get("note_id")
op_type = req_data.get("op_type")
current_user = req_data.get("current_user")
# 前置鉴权,无权限直接拦截,节省AI token消耗
auth_result = get_note_with_auth(note_id, current_user)
if not auth_result["ok"]:
return {"ok": False, "msg": auth_result["msg"]}
note_info = auth_result["data"]
origin_title = note_info["title"]
origin_content = note_info["content"]
# 区分摘要 / 润色两种提示词
if op_type == "summary":
system_prompt = "你是文本摘要助手,提炼原文核心信息,精简总结输出。"
elif op_type == "polish":
system_prompt = "你是文本润色助手,优化语句通顺度,保留原文全部核心信息。"
else:
return {"ok": False, "msg": "非法操作类型"}
# SSE流式生成器
def stream_generator():
full_text = ""
ai_api_url = "你的大模型流式接口地址"
api_key = "你的API_KEY"
payload = {
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"标题:{origin_title}\n内容:{origin_content}"}
],
"stream": True,
"temperature": 0.6
}
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
try:
resp = requests.post(ai_api_url, json=payload, headers=headers, stream=True, timeout=30)
for chunk in resp.iter_lines():
if chunk:
chunk_data = chunk.decode("utf-8")
text_segment = parse_chunk(chunk_data)
if text_segment:
full_text += text_segment
yield f"data: {text_segment}\n\n"
# 流式结束,完整结果写入Bmob
BmobQuery("Note").create({
"title": f"{origin_title}【AI处理】",
"content": full_text,
"user": current_user
})
yield f"data: [DONE]\n\n"
except Exception as e:
yield f"data: [ERROR]{str(e)}\n\n"
return Response(stream_with_context(stream_generator()), mimetype="text/event-stream")
三、前端JS代码,监听SSE、打字机渲染
ini
async function startAiStream(noteId, opType) {
// 按钮锁,防止重复多次提交请求
const aiBtn = document.querySelector("#aiBtn");
const resultDom = document.querySelector("#aiResult");
aiBtn.disabled = true;
resultDom.innerText = "";
const eventSource = new EventSource(`/api/ai-stream?note_id=${noteId}&op_type=${opType}`);
eventSource.onmessage = (e) => {
const msg = e.data;
if(msg === "[DONE]"){
eventSource.close();
showToast("生成完成,新笔记已保存至Bmob");
aiBtn.disabled = false;
return;
}
if(msg.startsWith("[ERROR]")){
eventSource.close();
showToast("生成失败:" + msg.replace("[ERROR]",""));
aiBtn.disabled = false;
return;
}
// 追加文本,实现打字机效果
resultDom.innerText += msg;
}
eventSource.onerror = () => {
eventSource.close();
showToast("连接中断,请重试");
aiBtn.disabled = false;
}
}
四、核心知识点解析
1. SSE 适用场景
SSE 是服务端单向推送,只需要服务端向客户端发消息,无需双向通信。 相比 WebSocket,开发成本更低,非常适合AI大模型流式输出场景。
2. 为什么等流式全部结束才写入Bmob
流式过程中仅做前端页面预览展示。 只有完整接收全部AI内容,才调用Bmob SDK新建笔记。 一旦中途网络断开、接口报错,不会将残缺文本入库,保障数据库数据干净。
3. 前置鉴权的意义
在调用大模型接口前,校验笔记归属。 未授权用户直接拦截,不会发起昂贵的大模型请求,节约token开销。 用户权限、数据隔离直接复用Bmob自带ACL能力,不用手写复杂权限底层代码。
4. 交互层面优化
- 按钮锁定,防止用户连续点击重复请求
- 区分多种状态:AI思考中、生成中、完成、异常失败
- 异常捕获:超时、网络中断、模型报错,前端给出友好提示
五、为什么选择Bmob后端云
整个项目没有自行搭建数据库、底层服务器。 用户体系、数据表存储、ACL数据权限隔离全部交给Bmob后端云,免费免开发。 开发者可以聚焦业务逻辑、AI交互开发,省去大量底层基础设施开发工作,适合个人练手、简历项目快速落地。
六、总结

很多新手写AI项目,只保证功能跑通,忽略交互反馈。 一次性返回只能叫"能用";加上SSE流式输出之后,项目具备完整产品交互质感。
本Demo包含权限校验、SSE流式接口、异常捕获、云存储,放到简历中对比简单CRUD项目,竞争力更强。