首页
All Posts

前端智能体开发(一):前端接入大模型:API、流式响应与多模态产品边界

这张图把一次大模型调用画成一条产品管线:请求协议负责把用户意图组织成结构化输入,流式响应让用户尽早看到结果,多模态模块处理图片、语音和视觉任务,工程边界负责鉴权、限流、日志与费用统计。真正的 AI 前端开发不是“浏览器里写一个 f...

大模型 API 产品管线

这张图把一次大模型调用画成一条产品管线:请求协议负责把用户意图组织成结构化输入,流式响应让用户尽早看到结果,多模态模块处理图片、语音和视觉任务,工程边界负责鉴权、限流、日志与费用统计。真正的 AI 前端开发不是“浏览器里写一个 fetch”这么简单,而是要把模型能力放进一个可靠的产品回合里。

一、从一次 fetch 看清大模型 API 的基本协议

大多数文本模型都提供 HTTP 接口。前端工程师最容易上手的方式,是向 /chat/completions 这类接口发送 JSON 请求,然后把返回内容显示到页面上。一个最小请求通常包含四类信息:

const payload = {
  model: "deepseek-chat",
  messages: [
    { role: "system", content: "你是一个耐心的前端助手。" },
    { role: "user", content: "解释一下什么是 token。" }
  ],
  stream: false
};

model 选择能力边界,messages 描述上下文,stream 决定传输方式,返回里的 usage 可以用来估算成本。这里最容易被忽视的是 messages 的结构。它不是随便拼接一段字符串,而是一份对话状态:

  • system 负责长期约束,例如角色、语气、输出格式、禁区。
  • user 是用户输入,也可以包含前端整理后的上下文。
  • assistant 可以作为历史回复进入上下文,帮助模型保持连续性。

一个普通问答页面只需要一条 user 消息;一个真实产品往往需要把用户画像、页面状态、业务规则、历史对话摘要和当前输入一起组织进去。这时就不能把所有内容粗暴塞进一个字符串,而要建立清晰的消息构造函数。

function buildMessages(input: string, profile: UserProfile, page: PageState) {
  return [
    {
      role: "system",
      content: [
        "你是一个面向初学者的前端学习助手。",
        "回答要具体,必要时给出代码片段。",
        "不确定时说明原因,不要编造 API。"
      ].join("\n")
    },
    {
      role: "user",
      content: JSON.stringify({
        input,
        level: profile.level,
        currentPage: page.name,
        selectedText: page.selection
      })
    }
  ];
}

这样做的好处是:提示词、业务数据和用户输入之间有清楚的边界。后续你要增加埋点、缓存、审计或测试,也更容易定位问题。

二、密钥不能放在浏览器里

很多入门 Demo 会把 API Key 放进 .env.local,再通过 import.meta.env 在浏览器代码里读取。这种方式适合本地学习,但不适合线上产品。原因很直接:浏览器端代码最终会被打包给用户,密钥很容易被看到、复制和滥用。

生产结构应该是:

  1. 浏览器把用户输入发给自己的后端。
  2. 后端读取服务端环境变量中的 API Key。
  3. 后端调用模型服务。
  4. 后端把模型结果转发给浏览器。

这个后端代理层不只是为了隐藏密钥,还承担至少六件事:用户鉴权、频率限制、模型路由、成本统计、日志审计、错误归一化。

例如,一个简单的代理接口可以这样设计:

app.post("/api/ai/chat", async (req, res) => {
  const user = await requireUser(req);
  await rateLimit(user.id, "ai-chat");

  const messages = buildMessages(req.body.input, user.profile, req.body.page);
  const result = await callModel({ messages, stream: false });

  await recordUsage({
    userId: user.id,
    model: result.model,
    tokens: result.usage.total_tokens
  });

  res.json({ content: result.choices[0].message.content });
});

前端只看到自己的业务接口,不需要知道第三方平台的密钥和内部协议。

三、流式响应解决的是“等待感”,不是只解决速度

如果一次推理需要十秒,普通 HTTP 响应会让用户十秒内什么都看不到。流式响应的价值不只是让结果更快到达,而是把“等待”变成“正在发生”。

从浏览器角度看,流式响应的核心动作是读取 ReadableStream

const reader = response.body?.getReader();
const decoder = new TextDecoder();
let buffer = "";

while (reader) {
  const { value, done } = await reader.read();
  if (done) break;

  const chunk = buffer + decoder.decode(value, { stream: true });
  buffer = "";

  for (const line of chunk.split("\n")) {
    if (!line.startsWith("data: ")) continue;
    const payload = line.slice(6);
    if (payload === "[DONE]") return;

    try {
      const data = JSON.parse(payload);
      const delta = data.choices?.[0]?.delta?.content;
      if (delta) appendToUI(delta);
    } catch {
      buffer += line;
    }
  }
}

这里有三个关键点。

第一,网络分片不等于 JSON 分片。有时一个 JSON 对象会被拆到两次 read() 里,所以需要 buffer 保存未解析完的片段。

第二,UI 更新要节流。模型每次返回几个字符,如果每个字符都触发一次复杂渲染,页面会卡。可以把增量内容放进队列,用 requestAnimationFrame 或 50ms 定时批量更新。

第三,用户可以取消。AI 页面经常会出现“我不想等了”“我问错了”“我切换页面了”的情况。后端和前端都要支持取消,否则会浪费 token,也会让旧请求覆盖新结果。

const controller = new AbortController();

fetch("/api/ai/chat-stream", {
  method: "POST",
  body: JSON.stringify({ input }),
  signal: controller.signal
});

// 用户点击停止
controller.abort();

流式体验做得好,用户会觉得系统在持续响应;做得差,就会变成跳字卡顿、重复输出、旧答案覆盖新答案。

四、多模态调用通常是“异步任务”,不是一次请求

文本问答大多可以直接返回结果。图像生成、语音合成、视觉识别则经常是另一种模式:提交任务,拿到任务 ID,轮询或订阅进度,最后拿结果。

以图像生成为例,前端不能只维护一个 imgUrl,还要维护任务状态:

type GenerationState =
  | { status: "idle" }
  | { status: "submitting" }
  | { status: "running"; progress: number; taskId: string }
  | { status: "done"; url: string }
  | { status: "failed"; reason: string };

这个状态模型能直接指导 UI:

  • submitting:按钮禁用,提示正在提交。
  • running:显示进度条和占位图。
  • done:展示结果,并提供重新生成、下载、使用到项目。
  • failed:给出可理解错误,而不是只显示 500

语音合成也类似。用户输入文本后,后端提交 TTS 任务,返回音频 URL 或二进制流。前端需要处理播放、暂停、加载失败、移动端自动播放限制。视觉模型则还要处理图片上传、压缩、格式校验和隐私提示。

一个常见错误是把多模态任务当成“换一个 endpoint”。其实多模态更像产品流程:文件准备、任务提交、进度反馈、结果处理、失败恢复,每一步都要设计。

五、用户体验要围绕“可预期”设计

AI 应用的不确定性比传统表单更高。模型可能慢,可能输出偏题,可能返回格式不稳定,也可能因为额度或安全策略失败。前端需要把这些不确定性包进可预期的交互。

一个实用的设计清单:

  1. 输入前:给示例问题、字数限制、文件格式限制。
  2. 提交后:立即出现状态,不要让按钮点完像没反应。
  3. 生成中:展示流式内容、进度或阶段,而不是单纯 spinner。
  4. 可取消:用户能停止当前任务。
  5. 可恢复:刷新页面后能找回任务状态或结果。
  6. 可追踪:后台能查到请求、模型、耗时、token 和错误。
  7. 可降级:主模型失败时,有备用模型或明确提示。

例如,一个“生成学习卡片”的页面可以这样拆状态:

type CardJob = {
  id: string;
  input: string;
  phase: "outline" | "cards" | "images" | "done" | "failed";
  cards: Array<{ title: string; body: string; image?: string }>;
  error?: string;
};

用户看到的不是“正在生成”,而是“正在整理知识点”“正在生成卡片文案”“正在生成配图”。这种阶段感会显著降低焦虑。

六、一个完整例子:AI 作文点评助手

假设要做一个面向学生的作文点评助手。它看起来只是一个文本模型调用,但真实实现会涉及完整产品链路。

前端输入:

  • 作文文本。
  • 年级。
  • 评分标准。
  • 是否需要逐段点评。

后端处理:

  • 过滤敏感信息。
  • 根据年级生成 system 约束。
  • 选择模型。
  • 用流式方式返回点评。
  • 统计 token 和耗时。

前端展示:

  • 总评先出来。
  • 分项评分逐步出现。
  • 修改建议以列表形式补齐。
  • 用户可以中途取消。
  • 结果保存到历史记录。

消息可以这样组织:

const messages = [
  {
    role: "system",
    content: [
      "你是作文点评助手。",
      "评价要鼓励具体,指出问题时给出修改方法。",
      "输出结构:总评、亮点、问题、修改建议、示例改写。"
    ].join("\n")
  },
  {
    role: "user",
    content: JSON.stringify({
      grade: "五年级",
      rubric: "内容、结构、语言、错别字",
      essay
    })
  }
];

流式 UI 不应该把所有内容塞进一个文本框。更好的做法是让模型输出结构化数据,或让后端根据阶段拆分输出:先显示总评,再显示分项,再显示示例改写。这样用户能在生成尚未结束时先读起来。

七、落地时最容易踩的坑

第一,把 API Key 放进浏览器。Demo 可以,线上不行。

第二,没有处理流式分片。直接 JSON.parse(chunk) 会遇到半截 JSON。

第三,没有请求取消。旧请求返回后覆盖新问题,是 AI 聊天产品里非常常见的 Bug。

第四,没有费用统计。用户增长后,token 成本可能比服务器成本更快失控。

第五,把多模态任务当同步接口。图片、语音、视觉都要考虑任务状态和失败恢复。

第六,只写成功路径。AI 服务更需要错误分层:鉴权失败、额度不足、模型超时、内容安全拦截、网络中断、解析失败,都应该有不同处理。

八、前端工程师应该建立的底层模型

接入大模型的第一层能力是会调用 API;第二层能力是会做流式体验;第三层能力是能把文本、图片、语音、视觉放进同一套产品状态机;第四层能力是能把安全、成本、日志和降级放到系统设计里。

一次可靠的大模型调用不是把请求发出去就结束,而是每个环节都知道自己的职责:协议如何组织输入,流式响应如何更新界面,多模态任务如何排队,鉴权、限流和日志如何兜底。真正决定质量的,是整套产品与工程体系能否稳定跑完一个回合。