AI 智能问答:从前端到后端的完整实践指南

Jiafeng

分类: WEB前端 0 0

AI 智能问答:从前端到后端的完整实践指南

总体架构

  • 前端组件:负责消息管理、发起请求、解析流、渲染 Markdown,并提供复制、折叠、停止、二次生成等交互。
  • 配置常量:集中声明 API 地址、鉴权信息与模型列表,供前端选择。
  • 后端代理(推荐):隐藏密钥,向上游转发请求并“直传流”,保证前端解析逻辑不变。

前端调用

  • 请求结构保持 OpenAI 风格:
    • model: 选择的模型,来源于 LLM_MODELS
    • messages: 数组格式的对话历史 { role, content }
    • stream: true: 启用流式输出,提高响应体验
  • 请求头包含 Content-Type: application/json。密钥不应出现在前端,推荐后端代理模式中由服务端注入。

示例请求体:

json
{
  "model": "DeepSeek-R1-0528-Qwen3-8B",
  "messages": [
    { "role": "system", "content": "你是一个有帮助的助手" },
    { "role": "user", "content": "介绍一下深度学习" }
  ],
  "stream": true
}

流式输出解析

  • 基于浏览器的 fetchReadableStream,逐块读取响应内容。
  • 解析策略:
    • 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 清理,确保安全。

交互设计

  • 消息列表:区分 userassistant 的视觉样式,支持背景视频叠加。
  • 复制与折叠:通过事件委托识别点击目标,支持复制代码文本与折叠展开。
  • 停止流:通过 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人 Love
  • 0人 Haha
  • 0人 Wow
  • 0人 Sad
  • 0人 Angry
AI

作者简介: Jiafeng

共 0 条评论关于 “AI 智能问答:从前端到后端的完整实践指南”

Loading...