AI 智能问答:从前端到后端的完整实践指南
分类: WEB前端 0 0
AI 智能问答:从前端到后端的完整实践指南
总体架构
- 前端组件:负责消息管理、发起请求、解析流、渲染 Markdown,并提供复制、折叠、停止、二次生成等交互。
- 配置常量:集中声明 API 地址、鉴权信息与模型列表,供前端选择。
- 后端代理(推荐):隐藏密钥,向上游转发请求并“直传流”,保证前端解析逻辑不变。
前端调用
- 请求结构保持 OpenAI 风格:
model: 选择的模型,来源于LLM_MODELSmessages: 数组格式的对话历史{ role, content }stream: true: 启用流式输出,提高响应体验
- 请求头包含
Content-Type: application/json。密钥不应出现在前端,推荐后端代理模式中由服务端注入。
示例请求体:
json
{
"model": "DeepSeek-R1-0528-Qwen3-8B",
"messages": [
{ "role": "system", "content": "你是一个有帮助的助手" },
{ "role": "user", "content": "介绍一下深度学习" }
],
"stream": true
}
流式输出解析
- 基于浏览器的
fetch与ReadableStream,逐块读取响应内容。 - 解析策略:
- 以
data:开头的行,去掉前缀并尝试JSON.parse,取choices[0].delta.content增量字符串。 - 处理结束标记
[DONE],用来收尾与恢复状态。 - 非
data:行视为某些实现返回的整块 JSON,取choices[0].message.content作为整段内容。
- 以
- 每次增量到达时,累加到当前
assistant消息并滚动到底部。
流式行示例:
text
data: {"choices":[{"delta":{"content":"深"}}]}
data: {"choices":[{"delta":{"content":"度"}}]}
data: {"choices":[{"delta":{"content":"学习"}}]}
data: [DONE]
Markdown 渲染与高亮
- 使用
markdown-it进行 Markdown 渲染,禁用原始 HTML,启用链接与换行。 - 使用
highlight.js做代码高亮,按需注册常见语言(JavaScript、TypeScript、Python、bash、JSON、XML、CSS)。 - 自定义代码块渲染,提供展示语言、复制与折叠功能。
- 使用
DOMPurify对渲染结果进行 XSS 清理,确保安全。
交互设计
- 消息列表:区分
user与assistant的视觉样式,支持背景视频叠加。 - 复制与折叠:通过事件委托识别点击目标,支持复制代码文本与折叠展开。
- 停止流:通过
AbortController中止当前流式请求,及时恢复输入与按钮状态。 - 二次生成:内置一个提示词优化流程,流式接收优化结果并写回输入框,提升“问得更好”的效果。
- 复制最后回答:便于将 AI 输出直接带走。
后端代理(推荐)
-
为什么需要代理:
- 密钥安全:避免在前端暴露鉴权信息
- 可控性与治理:可添加配额、速率限制、日志、审计与白名单
- 灵活适配:可统一处理上游模型差异与错误格式
-
目标接口:
POST /api/chat- 请求体透传
model/messages/stream - 服务端从环境变量读取上游密钥
- 设置
Content-Type: text/event-stream,直传流式响应
- 请求体透传
-
Node/Express 示例:
js
import express from 'express';
import fetch from 'node-fetch';
const app = express();
app.use(express.json());
app.post('/api/chat', async (req, res) => {
const upstream = await fetch('https://api.xxx', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.EDGEFN_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(req.body)
});
if (!upstream.ok) {
res.status(upstream.status).send(await upstream.text());
return;
}
res.setHeader('Content-Type', 'text/event-stream');
upstream.body.pipe(res);
});
app.listen(3001);
- Nuxt 3 示例:
ts
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const upstream = await fetch('https://api.edgefn.net/v1/chat/completions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.EDGEFN_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (!upstream.ok) {
setResponseStatus(event, upstream.status);
return await upstream.text();
}
setResponseHeaders(event, { 'Content-Type': 'text/event-stream' });
const reader = upstream.body!.getReader();
const stream = new ReadableStream({
async start(controller) {
while (true) {
const { value, done } = await reader.read();
if (done) break;
controller.enqueue(value);
}
controller.close();
}
});
return stream;
});
切换到代理的前端改造
- 将前端
AI_API_URL指向'/api/chat'。 - 移除前端的
Authorization头。 - 请求体与解析逻辑完全复用;前端依旧按
data:与[DONE]解析即可。
错误与中止处理
- 请求失败:检查
res.ok,失败时提示错误并恢复状态。 - 中止请求:捕获
AbortError,不作为失败提示。 - 解析容错:遇到不可解析的行,降级为直接拼接文本,保证视觉连续性。
共 0 条评论关于 “AI 智能问答:从前端到后端的完整实践指南”