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

这张图把一次大模型调用画成一条产品管线:请求协议负责把用户意图组织成结构化输入,流式响应让用户尽早看到结果,多模态模块处理图片、语音和视觉任务,工程边界负责鉴权、限流、日志与费用统计。真正的 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 在浏览器代码里读取。这种方式适合本地学习,但不适合线上产品。原因很直接:浏览器端代码最终会被打包给用户,密钥很容易被看到、复制和滥用。
生产结构应该是:
- 浏览器把用户输入发给自己的后端。
- 后端读取服务端环境变量中的 API Key。
- 后端调用模型服务。
- 后端把模型结果转发给浏览器。
这个后端代理层不只是为了隐藏密钥,还承担至少六件事:用户鉴权、频率限制、模型路由、成本统计、日志审计、错误归一化。
例如,一个简单的代理接口可以这样设计:
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 应用的不确定性比传统表单更高。模型可能慢,可能输出偏题,可能返回格式不稳定,也可能因为额度或安全策略失败。前端需要把这些不确定性包进可预期的交互。
一个实用的设计清单:
- 输入前:给示例问题、字数限制、文件格式限制。
- 提交后:立即出现状态,不要让按钮点完像没反应。
- 生成中:展示流式内容、进度或阶段,而不是单纯 spinner。
- 可取消:用户能停止当前任务。
- 可恢复:刷新页面后能找回任务状态或结果。
- 可追踪:后台能查到请求、模型、耗时、token 和错误。
- 可降级:主模型失败时,有备用模型或明确提示。
例如,一个“生成学习卡片”的页面可以这样拆状态:
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;第二层能力是会做流式体验;第三层能力是能把文本、图片、语音、视觉放进同一套产品状态机;第四层能力是能把安全、成本、日志和降级放到系统设计里。
一次可靠的大模型调用不是把请求发出去就结束,而是每个环节都知道自己的职责:协议如何组织输入,流式响应如何更新界面,多模态任务如何排队,鉴权、限流和日志如何兜底。真正决定质量的,是整套产品与工程体系能否稳定跑完一个回合。