AI 产品体验
副标题:流式交互、中断续写、失败兜底与可解释
目标:把“不确定的 AI 输出”包装成“稳定、好用、可控”的用户体验。这也是前端在 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 的刷新间隔体感最好,能兼顾“像打字”与“不卡顿”。
中断 / 继续 / 重试:三个按钮背后的状态机
一个实用的聊天消息状态机(建议你按这个设计 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”,方便反馈
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