返回文章
AI约 4 分钟阅读

前端接入 AI:接口之外,还要处理这些交互边界

密钥放在哪里,如何停止生成,失败后保留什么?从一个简化的请求示例梳理 AI 功能的前后端职责。

GUAN前端开发手记
示例文章

收藏保存在此浏览器,可在归档页查看。

文章目录4 个章节

一个聊天输入框很容易做出来,但可用的 AI 功能还需要处理等待、取消、失败和重复提交。先约定接口与状态,再连接模型服务,能减少“正常时能跑、异常时失控”的情况。

密钥留在服务端

浏览器里的代码、请求和构建产物都能被查看。放进前端环境变量不等于保密,带有客户端公开前缀的变量通常会被打包进页面。前端应该调用自己的服务端接口,由服务端持有模型供应商密钥。

代理接口仍需验证登录状态、授权、输入长度,并按用户设置请求频率、并发和额度限制。仅仅隐藏密钥却开放无限调用,会把费用风险留在自己账户上。日志也应避免完整记录敏感提示词和生成内容。

从明确的请求契约开始

下面约定 /api/ai 返回 { text: string }。它是需要自行实现的后端接口,本站没有提供该服务;示例只演示前端请求控制,也没有绑定某家模型 API。

let activeController = null;

async function generate(prompt) {
  if (activeController) return; // 阻止重复提交
  const controller = new AbortController();
  activeController = controller;
  setStatus('生成中');
  try {
    const response = await fetch('/api/ai', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ prompt }),
      signal: controller.signal,
    });
    if (!response.ok) throw new Error('请求未成功');
    const data = await response.json();
    if (typeof data.text !== 'string') throw new Error('响应格式错误');
    output.textContent = data.text;
    setStatus('已完成');
  } catch (error) {
    setStatus(controller.signal.aborted ? '已停止' : '生成失败,请重试');
  } finally {
    activeController = null;
  }
}

function stop() {
  activeController?.abort();
}

output 代表结果容器,setStatus 代表界面状态更新函数,需要在应用中实现。发送期间还应禁用发送按钮,同时保留“停止生成”按钮。中止浏览器请求不保证上游立即停止计费,服务端也要转发取消信号。

流式输出多了一层状态

如果改用 SSE 或流式 fetch,需要明确消息格式、完成信号和错误事件。网络分块不等于完整消息:一个 JSON 事件可能拆成几段,多个事件也可能一次到达,应使用缓冲和对应协议解析器。

取消后保留已生成文字并标记“已停止”;中途断流应提示“内容未完成”,不要显示成功。为每轮请求分配标识,避免晚到的旧响应覆盖新对话。自动滚动也应尊重用户:用户向上阅读时暂停跟随,提供“回到最新”入口。

输出先当作不可信内容

不要把模型返回的 HTML 直接交给 innerHTML。纯文本使用 textContent;需要 Markdown 时,关闭原始 HTML 或使用可靠清洗方案,并检查链接协议。模型给出的代码和链接也需要审查,不能因为来自模型就自动执行。

上线前至少验证慢网、断网、限流、空响应、连续点击和取消。错误时保留用户输入并允许主动重试;未经确认的自动重试可能产生重复内容和额外费用。让界面说清楚当前状态,比始终转圈更有帮助。

读到这里,不妨动手试一试。
把一个概念变成能运行的小实验。

找一篇笔记

输入关键词,或浏览全部文章。

按 Tab 选择文章Esc 关闭