AI 产品体验
副标题:流式交互、中断续写、失败兜底与可解释
目标:把“不确定的 AI 输出”包装成“稳定、好用、可控”的用户体验。这也是前端在 AI 产品里最稀缺的价值。
目录
- 体验为什么决定胜负
- 流式输出:打字机效果只是起点
- 中断 / 继续 / 重试:三个按钮背后的状态机
- 失败兜底:超时、限流、内容不合规、模型不可用
- 可解释与可追溯:引用来源、证据片段与操作日志
- Generative UI(生成式界面):让 AI 输出“可交互组件”而非纯文本
- Prompt 可视化与可配置:把“魔法”变成“产品能力”
- 最小可运行 Demo(体验版)
- 完整可运行代码(流式体验示例)
体验为什么决定胜负
很多 AI 产品“能力差”,本质不是模型差,而是体验没兜住:
- 等半天没反应,用户以为卡死
- 一直输出,用户停不下来
- 输出一半报错,用户不知道怎么办
- 答案看起来对,但无法验证来源
前端 + AI 的核心价值是:把模型能力转化为可感知、可控制、可恢复的交互。
流式输出:打字机效果只是起点
流式输出要解决的不是“显示”,而是“控制”
至少要覆盖:
- 实时反馈:首 token 时间(TTFT)越低越好
- 节流渲染:避免每个 token 都触发重渲染
- 增量渲染策略
- 文本:直接增量拼接 + 节流刷新
- Markdown:建议“流式阶段先纯文本”,结束后再一次性 Markdown 渲染(或分段渲染)
体验细节(非常加分)
- 显示“正在思考/正在检索/正在生成”的阶段提示(尤其 RAG)
- 显示“已生成字数/预计剩余”(可选)
- 支持“复制本条回答”“复制引用来源”
示例:前端节流渲染(避免每个 token 触发渲染)
let buffer = "";
let lastFlush = 0;
const FLUSH_INTERVAL = 80; // ms
function onDelta(text: string) {
buffer += text;
const now = Date.now();
if (now - lastFlush > FLUSH_INTERVAL) {
flush();
lastFlush = now;
}
}
function flush() {
if (!buffer) return;
// 这里替换成 setState/emit 逻辑
appendToUI(buffer);
buffer = "";
}
经验值:50-120ms 的刷新间隔体感最好,能兼顾“像打字”与“不卡顿”。
推理模型的体验:把“漫长等待”变成“看得见的思考”
接入推理模型(o 系列 / R1 / thinking 模式)时,首字延迟(TTFT)可能长达数秒甚至几十秒,因为模型在“思考”。如果只显示一个转圈,用户会以为卡死。
体验设计要点:
- 思考阶段可视化:显示“正在思考 / 正在分析 / 正在规划”,有条件就展示思考摘要(多数厂商只给摘要,不给完整原始思维链)。
- 思考过程可折叠:做一个默认收起的“思考过程”面板(类似 ChatGPT/Claude 的 reasoning 折叠区),想看的人能展开。
- 分阶段流式:先流“思考摘要”,再流“正式答案”,两段视觉上区分开。
- 成本与时延预期:对耗时长的推理任务,给“预计需要更久”的提示,降低焦虑。
这一段是“懂推理模型”的体验细节,面试里能体现你不是只会接普通 chat。
中断 / 继续 / 重试:三个按钮背后的状态机
一个实用的聊天消息状态机(建议你按这个设计 UI/数据结构):
- idle:未开始
- streaming:正在流式输出
- stopped:用户主动停止
- failed:失败(超时/限流/网络/模型错误)
- done:完成
对应用户操作:
- 停止:streaming → stopped
- 继续生成:stopped → streaming(携带“已生成内容”作为上下文继续)
- 重试:failed → streaming(可带上同一输入、不同策略)
工程要点:
- 停止一定要“真的停止”:
- 前端中止请求
- 服务端也要尽快中止对模型的流式拉取(避免继续烧 Token)
- 并发控制:
- 同一会话一次只允许一个 streaming(或给用户明确提示)
图示:消息状态机(Mermaid)
并发与会话策略(一定要写清楚)
- 同会话单流式:避免“一问多答”的 UI 混乱
- 跨会话并发上限:例如每用户最多 2 条流式并发
- 停止/继续的上下文策略
- 停止时保留已生成内容
- 继续时把“已生成内容 + 原问题”一起作为上下文
失败兜底:超时、限流、内容不合规、模型不可用
1)超时
用户视角:不要只显示“错误”,要告诉他能做什么:
- “已生成内容保留”
- “你可以:继续生成 / 重新生成 / 缩短问题 / 稍后再试”
UI 文案示例(可直接用)
- 标题:生成超时
- 正文:已保留当前内容。你可以点击“继续生成”,或缩短问题再试。
2)限流/配额不足
这是 AI 产品最常见失败之一,建议在 UI 上做出“可理解”的提示:
- 是“并发太多”还是“今日额度用完”
- 引导升级/稍后再试/减少输出长度
UI 文案示例
- 标题:请求过于频繁
- 正文:当前并发已满,请稍后重试,或先结束其他对话。
3)内容不合规
如果你接入的是有内容安全策略的模型:
- 明确告诉用户“哪些部分不支持”
- 给出可替代建议(比如“我可以改成科普/非敏感表达”)
UI 文案示例
- 标题:内容不符合使用规范
- 正文:我无法处理该请求,但可以帮你改为科普/非敏感的表达方式。
4)模型不可用
做“多模型降级”:
- 主模型失败 → 备用模型(能力可能弱,但可用)
- UI 显示“已自动切换模型”(透明且可控)
UI 文案示例
- 标题:模型暂不可用
- 正文:已自动切换到备用模型,结果可能略有差异。
可解释与可追溯:引用来源、证据片段与操作日志
对用户来说,“答案对不对”有时很难判断;但“证据从哪来”更容易判断。
你可以在 UI 上提供:
- 引用来源:每段回答对应哪些文档片段
- 证据片段:可展开查看 chunk 内容
- 定位到原文:如果有页码/段落定位就更好
对工程来说(排障与审计):
- 每次回答都带 traceId
- 记录:检索到的 docId/chunkId、模型参数、耗时、错误码(脱敏)
透明化设计:让用户“看到过程”
- 检索型回答展示“证据来源列表”
- 工具型回答展示“执行步骤(可折叠)”
- 失败时显示“错误码 + traceId”,方便反馈
Generative UI(生成式界面):让 AI 输出“可交互组件”而非纯文本
2024 年后前端 AI 的一个重要趋势:不再只把模型输出渲染成文字/Markdown,而是根据模型的结构化输出/工具调用,动态渲染出真正的 React 组件——天气卡片、订单表格、可点击的确认按钮、图表等。
核心思路:
- 模型负责“决定展示什么 + 提供结构化数据”(通过工具调用或结构化输出),前端负责“把数据映射成组件”。
- 例如模型调用
getWeather工具 → 前端不显示 JSON,而是渲染一个<WeatherCard>。 - 用 Vercel AI SDK 时,
useChat的 message parts 里就带有tool-xxx类型的 part,可据此渲染对应组件。
// 根据消息 part 类型渲染不同 UI(Generative UI 思路)
{message.parts.map((part, i) => {
switch (part.type) {
case "text":
return <MarkdownView key={i} content={part.text} />;
case "tool-getWeather":
return part.state === "output-available"
? <WeatherCard key={i} data={part.output} />
: <Skeleton key={i} />;
default:
return null;
}
})}
价值(面试讲法):
- 可交互:用户能在 AI 回答里直接点按钮、填表单、确认操作(human-in-the-loop)。
- 信息密度高:表格/图表/卡片比一大段文字更易读。
- 可控安全:组件由你写死,避免模型直接输出 HTML 带来的 XSS 风险(对照 Markdown 协议)。
Prompt 可视化与可配置:把“魔法”变成“产品能力”
不要把 Prompt 写死在代码里(尤其是产品化后),建议提供:
- Prompt 模板选择:不同任务不同模板
- 关键参数可调:语气、长度、是否要引用、输出格式
- 版本管理:改 Prompt 就像改配置,能回滚
用户体验层可以这样设计:
- “高级设置”抽屉:展示关键参数
- “提示词预览”只读区域:让用户知道系统在做什么(增强信任)
最小可视化组件清单
- 模型选择器(主/备模型)
- 稳定性滑杆(温度映射)
- 输出长度选项(短/中/长)
- 是否引用来源(RAG 开关)
最小可运行 Demo(体验版)
目标:做出一个“能流式+能停止+有失败兜底”的最小交互体验。
最小可运行代码(UI 状态机 + 取消)
type State = "idle" | "streaming" | "stopped" | "failed" | "done";
let state: State = "idle";
function send() {
state = "streaming";
// start stream...
}
function stop() {
// abort stream...
state = "stopped";
}
function retry() {
state = "streaming";
}
完整 Demo 结构(最小体验骨架)
demo-ai-ux/
src/
App.tsx
ChatMessage.tsx
useStream.ts
uiStates.ts
常见坑排查清单
- 状态错乱:多次点击“发送/停止”无防抖 → 加按钮禁用/节流
- Markdown 卡顿:流式阶段先纯文本,结束后再渲染
- 兜底文案缺失:只显示“错误” → 加可操作提示
完整可运行代码(流式体验示例)
源码目录:
docs/demos/ai-chat-demo/web
cd docs/demos/ai-chat-demo/web
npm i
npm run dev