AI 应用的 UX 设计原则——工程师视角的 5 条
AI 应用的 UX 设计原则——工程师视角的 5 条
适读人群:做 AI 产品的工程师 / 关注 AI 体验的开发者 | 阅读时长:约14分钟 | 核心价值:不是设计师的设计原则,是工程师角度看 AI 体验的 5 个关键问题
我有段时间沉迷于看用户的屏幕录像(在用户同意的情况下,通过我们产品内置的行为录制功能)。
有一次我看到一个用户在用我们的 AI 助手。他发了一个问题,等待界面出现,等了大概 3 秒,他开始动鼠标,点了点别的地方,又点回来——他不知道 AI 是不是在处理。又等了 3 秒,他点了刷新页面。
那次 AI 请求其实只需要 5 秒,但他 6 秒就刷新了,因为他不知道到底有没有在处理。
我们的设计太差了。一个旋转的 loading 圈,没有任何提示,没有进度感。用户没有等待的理由。
那之后我开始认真想 AI 应用的 UX 问题,不是从设计师视角(配色、字体、布局),而是从工程师视角——这些体验问题背后的实现选择。
原则1:让等待有意义——加载中的体验设计
AI 响应慢是客观事实,但"慢"本身不是最大的问题,"不知道还要等多久"才是最大的问题。
不要只放一个转圈
转圈只传达一个信息:在处理。没有进度感,用户不知道已经等了多久、还要等多久。超过 2 秒就会开始焦虑。
三层递进的等待体验:
第一层(0-1秒): 立刻给反馈,告诉用户请求已收到
// 用户点击发送后立刻响应
handleSubmit() {
// 立刻禁用输入框,防止重复提交
this.inputDisabled = true;
// 立刻在聊天界面显示用户的消息(乐观更新)
this.messages.push({
role: 'user',
content: this.inputText,
timestamp: Date.now()
});
// 立刻显示 AI 的"思考中"占位符
this.messages.push({
role: 'assistant',
content: '',
status: 'thinking', // 显示"正在思考..."
timestamp: Date.now()
});
// 发起 AI 请求(流式)
this.callAiStream(this.inputText);
}第二层(1-3秒): 如果用了流式响应,第一个 token 一出来立刻渲染,不要等全部生成完。这让用户感知到"确实在处理"。
// Spring AI 流式调用,token 一出来就发给前端
chatClient.prompt()
.user(userMessage)
.stream()
.content()
.doOnNext(token -> {
// 每有一个 token 就推给前端
sseEmitter.send(SseEmitter.event()
.name("token")
.data(token));
})
.doOnComplete(() -> {
sseEmitter.send(SseEmitter.event().name("done").data("[DONE]"));
sseEmitter.complete();
})
.subscribe();第三层(超过5秒): 显示进度提示,告诉用户大概还需要多久。如果是 RAG,可以显示当前阶段:
// 根据 AI 请求的不同阶段,显示不同的进度提示
const STAGE_MESSAGES = {
'searching': '正在搜索相关资料...',
'reading': '正在阅读 3 篇参考文档...',
'generating': '正在生成回答...',
};
// 后端在不同阶段发送 SSE 事件
sseEmitter.send(SseEmitter.event()
.name("stage")
.data("searching"));这比一个转圈要好得多。用户知道发生了什么,等待变得有意义。
原则2:错误信息要说人话——不要把技术错误直接给用户看
这是我最常见到的问题。AI 调用失败了,直接把错误码或者技术异常信息展示给用户:
Error: 429 Too Many Requests
Request failed with status code 503
ECONNRESET at TCPConnectWrap.afterConnect用户看到这些完全不知道该怎么办。
错误信息设计的三个要素:
- 发生了什么(用户能理解的语言)
- 用户可以做什么(给出可操作的建议)
- 这是临时的还是持久的(让用户知道是否值得等待)
@ControllerAdvice
public class AiExceptionHandler {
@ExceptionHandler(AiException.class)
public ResponseEntity<ErrorResponse> handleAiException(AiException e) {
ErrorResponse response = translateToUserFriendlyError(e);
return ResponseEntity
.status(response.getHttpStatus())
.body(response);
}
private ErrorResponse translateToUserFriendlyError(AiException e) {
return switch (e.getErrorCode()) {
case "RATE_LIMIT_EXCEEDED" -> ErrorResponse.builder()
.userMessage("AI 服务当前请求量较大,请稍等片刻再试")
.actionSuggestion("通常 1-2 分钟后可以正常使用")
.isTemporary(true)
.retryAfterSeconds(60)
.build();
case "CONTEXT_LENGTH_EXCEEDED" -> ErrorResponse.builder()
.userMessage("您的问题或对话历史太长,AI 无法处理")
.actionSuggestion("请尝试开启新对话,或者缩短问题内容")
.isTemporary(false)
.build();
case "CONTENT_FILTER" -> ErrorResponse.builder()
.userMessage("您的请求包含了不符合使用规范的内容,无法处理")
.actionSuggestion("请修改问题后重新尝试")
.isTemporary(false)
.build();
case "TIMEOUT" -> ErrorResponse.builder()
.userMessage("AI 响应超时,可能因为问题比较复杂")
.actionSuggestion("可以尝试把问题拆分成几个更小的问题分别提问")
.isTemporary(true)
.retryAfterSeconds(5)
.build();
case "SERVICE_UNAVAILABLE" -> ErrorResponse.builder()
.userMessage("AI 服务暂时不可用,我们已收到通知正在处理")
.actionSuggestion("请稍后重试,通常会在 30 分钟内恢复")
.isTemporary(true)
.retryAfterSeconds(300)
.build();
default -> ErrorResponse.builder()
.userMessage("处理您的请求时遇到了问题")
.actionSuggestion("请刷新页面后重试,如果持续出现请联系客服")
.isTemporary(true)
.retryAfterSeconds(10)
.build();
};
}
}前端根据 isTemporary 和 retryAfterSeconds 来决定是否自动重试,以及是否显示"X秒后自动重试"的倒计时。
原则3:展示不确定性——AI 不是全知全能的,别装作是
AI 会犯错,会生成不准确的内容。把这个事实藏起来,不如坦诚地展示。
有意思的是,坦诚地展示不确定性,反而会提升用户信任度。用户知道这个 AI 不是在瞎自信,所以对它给出的内容更认可。
几种展示不确定性的方式:
置信度标签:
// 后端返回 AI 回答时,同时返回置信度评估
{
"answer": "根据当前文档,该配置项默认值为 30 秒",
"confidence": "HIGH", // HIGH / MEDIUM / LOW
"confidenceReason": "找到了 3 篇高度相关的文档,内容一致"
}
// 前端根据置信度显示不同样式
const confidenceConfig = {
HIGH: { icon: "✓", color: "green", label: "较为确定" },
MEDIUM: { icon: "~", color: "orange", label: "仅供参考" },
LOW: { icon: "?", color: "red", label: "不确定,请核实" }
};信息来源引用:
// RAG 场景,展示答案来自哪里
<div class="ai-answer">
<p>{{ answer }}</p>
<div class="sources" v-if="sources.length > 0">
<span class="source-label">参考资料:</span>
<a v-for="source in sources"
:href="source.url"
target="_blank"
class="source-link">
{{ source.title }}(第{{ source.page }}页)
</a>
</div>
</div>"AI 可能不准确"的提示位置很重要
有些产品把"AI 可能犯错"的提示藏在角落里,字很小,颜色很淡。这是在骗自己,也是在骗用户。
正确的做法:在 AI 可能给出关键决策建议时,用显眼的方式提醒:
// 当 AI 回答涉及敏感领域时,显示免责提示
if (isAnswerInSensitiveDomain(answer)) {
// 不是用小字灰色提示,而是用正常字体的对话泡
messages.push({
role: 'assistant',
content: `**请注意:** 以下内容仅供参考,不构成专业建议。如需准确信息,请咨询相关专业人士。`,
type: 'disclaimer'
});
}原则4:AI 建议 vs 用户决策——边界要清晰
这是我认为最重要、也最常被忽视的一条原则。
AI 应该辅助用户决策,而不是代替用户决策。但很多 AI 功能的设计,不自觉地把决策权从用户手里拿走了。
错误的设计模式(AI 替用户决策):
用户点击"AI 优化"
--> AI 直接修改了文档内容
--> 用户只能撤销或接受这个模式把用户变成了 AI 的审核员,而不是使用者。当 AI 改得不好时,用户心理上会产生抵触。
正确的设计模式(AI 辅助用户决策):
用户点击"AI 建议"
--> AI 给出修改建议
--> 清晰地展示原文 vs 建议的对比
--> 用户选择:接受 / 拒绝 / 部分采纳 / 让 AI 重新生成
--> 用户点击"接受"后,才真正修改代码实现的关键点:
// 后端返回 AI 建议时,同时返回原内容和建议内容,方便前端做 diff 对比展示
@Data
public class AiSuggestionResponse {
private String originalContent; // 原始内容
private String suggestedContent; // AI 建议内容
private String explanation; // AI 为什么这么建议(增加透明度)
private String suggestionId; // 用于后续的采纳/拒绝记录
private List<DiffBlock> diffs; // 具体改了哪些地方(高亮显示)
}// 前端展示 diff,让用户清楚看到变化
function renderDiff(diffs) {
return diffs.map(block => {
if (block.type === 'unchanged') {
return `<span>${block.content}</span>`;
} else if (block.type === 'removed') {
return `<del class="diff-removed">${block.content}</del>`;
} else if (block.type === 'added') {
return `<ins class="diff-added">${block.content}</ins>`;
}
}).join('');
}部分采纳的设计也很重要
AI 给出了一段建议,用户觉得第一半好,第二半不好。如果只能"全接受"或"全拒绝",用户体验会很差。设计上要支持段落级别的选择性采纳。
原则5:保留用户的撤销权——任何 AI 操作都要可逆
这条说起来简单,但真正做到的产品不多。
任何 AI 对用户数据的修改,都必须可以撤销。不只是 Ctrl+Z 那种本地撤销,而是服务端有记录,用户可以随时回到 AI 操作前的状态。
@Service
public class AiOperationHistoryService {
/**
* 在执行 AI 操作之前,先保存快照
*/
public String saveSnapshot(String userId, String resourceId, String resourceType,
String currentContent) {
String snapshotId = UUID.randomUUID().toString();
AiOperationSnapshot snapshot = AiOperationSnapshot.builder()
.snapshotId(snapshotId)
.userId(userId)
.resourceId(resourceId)
.resourceType(resourceType)
.contentBefore(currentContent)
.createdAt(Instant.now())
.build();
snapshotRepository.save(snapshot);
return snapshotId;
}
/**
* 执行 AI 操作后,更新快照,记录修改后的内容
*/
public void recordAiChange(String snapshotId, String newContent,
String aiOperationDescription) {
AiOperationSnapshot snapshot = snapshotRepository.findById(snapshotId)
.orElseThrow();
snapshot.setContentAfter(newContent);
snapshot.setOperationDescription(aiOperationDescription);
snapshotRepository.save(snapshot);
}
/**
* 用户申请回滚
*/
public void rollback(String userId, String snapshotId) {
AiOperationSnapshot snapshot = snapshotRepository.findById(snapshotId)
.orElseThrow();
// 验证权限
if (!snapshot.getUserId().equals(userId)) {
throw new ForbiddenException("无权回滚此操作");
}
// 执行回滚
resourceService.updateContent(snapshot.getResourceId(), snapshot.getContentBefore());
// 记录回滚事件(用于分析 AI 质量)
metricsService.recordRollback(snapshotId, "USER_INITIATED");
}
}用户知道可以撤销,就敢于尝试 AI 功能。心理安全感是 AI 功能被采用的前提。
这 5 条的共同底层
等待体验、错误信息、不确定性展示、决策边界、撤销权——这 5 条原则背后有一个共同的底层:尊重用户的感知和控制感。
AI 应用和普通软件的最大区别是:AI 的输出是不可预测的。用户面对不可预测的东西,本能地需要更多的信息和控制权。如果产品把这些都藏起来,用户就会觉得不安全,不会真正使用这些功能。
工程师做 UX 决策,核心不是美感,而是信任感。
