AI Agent 的用户体验设计:loading 状态、错误提示和置信度展示的最佳实践
AI Agent 的用户体验设计:loading 状态、错误提示和置信度展示的最佳实践
一、最好的 AI 能力毁于最差的 UX
说出来你可能不信:我们 AI CLI 工具 dayuan 的反馈邮件中,有 38% 不是在抱怨"输出不准",而是"要不要等这么久?""它卡住了?""我的结果在哪?"
自学转码让我天然对"用户怎么想"比"代码怎么写"更敏感。一个程序员往往就是用户自己——我知道等待一个黑盒模型吐结果时有多煎熬。
这篇文章我会分享在 CLI 和终端 UI 场景下,为 AI Agent 设计交互体验的三层框架。这些经验来自真实用户反馈和 A/B 测试的数据,不是凭空的设计原则。
二、Loading 状态设计:给等待赋予意义
2.1 问题诊断
2.2 实现:多阶段进度提示
use indicatif::{ProgressBar, ProgressStyle};
use std::time::Duration;
/// AI Agent 任务执行器
/// 核心设计:将长任务拆解为多个阶段,每阶段独立展示进度
struct AITaskExecutor {
/// 进度条组件,用于展示整体任务进度
progress: ProgressBar,
}
/// 定义任务的阶段
/// 每个阶段有独立的描述文本,让用户知道"系统在做什么"
enum TaskStage {
ParsingInput, // 解析用户输入
ContextRetrieval, // 检索相关上下文
ModelInference, // 模型推理
PostProcessing, // 后处理(格式化、校验等)
Complete, // 完成
}
impl AITaskExecutor {
fn new() -> Self {
// 创建多阶段进度条
let pb = ProgressBar::new(4);
pb.set_style(
ProgressStyle::default_bar()
.template("{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len}")
.unwrap()
.progress_chars("#>-"),
);
AITaskExecutor { progress: pb }
}
fn execute(&self, input: &str) -> Result<String, Box<dyn std::error::Error>> {
// 阶段1:解析输入
self.progress.set_message("🔍 正在解析输入内容...");
self.progress.set_position(0);
std::thread::sleep(Duration::from_millis(200)); // 模拟解析
// 阶段2:检索上下文
self.progress.set_message("📚 正在检索相关上下文...");
self.progress.inc(1);
std::thread::sleep(Duration::from_millis(500)); // 模拟检索
// 阶段3:模型推理(这是最慢的阶段)
self.progress.set_message("🤖 模型推理中...");
self.progress.inc(1);
self.streaming_inference(input)?;
// 阶段4:后处理
self.progress.set_message("✅ 正在整理输出结果...");
self.progress.inc(1);
std::thread::sleep(Duration::from_millis(200)); // 模拟后处理
self.progress.finish_with_message("✨ 完成!");
Ok("生成的结果文本...".to_string())
}
/// 流式输出的核心逻辑
/// 逐 token 展示而非等待全部完成,大幅改善等待体验
fn streaming_inference(&self, _input: &str) -> Result<(), Box<dyn std::error::Error>> {
// 使用终端流式输出的简化版示意
// 实际项目中使用 SSE 或 WebSocket 流
let tokens = ["你好", ",", "我", "是", "AI", "助手"];
for token in &tokens {
print!("{}", token);
std::io::Write::flush(&mut std::io::stdout())?;
// 模拟 token 生成间隔
std::thread::sleep(Duration::from_millis(50));
}
println!();
Ok(())
}
}
2.3 数据驱动的 UX 决策
我们在 500 个用户中做了 A/B 测试:
| 方案 | 用户焦虑率 | 任务放弃率 | 满意度 |
|---|---|---|---|
| 无提示(白屏等待) | 62% | 41% | 2.1/5 |
| 简单 spinner | 47% | 26% | 3.2/5 |
| 阶段提示 | 28% | 14% | 4.1/5 |
| 阶段提示 + 流式 | 15% | 7% | 4.6/5 |
结论很清楚:让用户知道你在做什么 + 让用户看到你正在产出,这两个设计可以直接把放弃率从 41% 降到 7%。
三、错误提示:好的错误信息是产品的第二张脸
3.1 错误分级体系
/// AI Agent 的错误类型分层
/// 核心原则:不同严重程度,不同展示策略
enum AgentError {
/// 1级 - 瞬时可恢复:网络抖动、超时重试
/// 策略:静默重试,仅在重试失败后提示
Transient {
message: String,
retry_count: u32,
max_retries: u32,
},
/// 2级 - 用户可修复:API Key 无效、配额不足、权限错误
/// 策略:清晰告知原因 + 给出修复指引
UserActionable {
message: String,
suggestion: String,
docs_url: Option<String>,
},
/// 3级 - 系统严重:模型过载、服务宕机
/// 策略:坦诚告知 + 预计恢复时间 + 替代方案
SystemCritical {
message: String,
estimated_recovery: Option<std::time::Duration>,
fallback: Option<String>,
},
}
impl std::fmt::Display for AgentError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AgentError::Transient { message, retry_count, max_retries } => {
write!(f, "⏳ {}(重试 {}/{})", message, retry_count, max_retries)
}
AgentError::UserActionable { message, suggestion, docs_url } => {
write!(f, "❌ {}\n 💡 {}", message, suggestion)?;
if let Some(url) = docs_url {
write!(f, "\n 📖 参考文档: {}", url)?;
}
Ok(())
}
AgentError::SystemCritical { message, estimated_recovery, fallback } => {
write!(f, "🚨 {}\n ", message)?;
if let Some(duration) = estimated_recovery {
write!(f, "预计恢复时间: {} 秒\n ", duration.as_secs())?;
}
if let Some(fb) = fallback {
write!(f, "替代方案: {}", fb)?;
}
Ok(())
}
}
}
}
三个原则:
- 告诉用户"为什么",而不是只告诉"失败了";
- 给用户一个下一步动作,而不是让他自己猜;
- 分级展示,不要用同样的严重度呈现"网络抖动重试"和"API Key 无效"。
线上数据:引入分级错误后,用户求助邮件中"不知道怎么修"的比例从 64% 降到了 22%。但有个意外的副作用:
Transient错误的静默重试次数太多(3次),用户看到 spinner 转 30 秒没反应直接关了终端。改成"首次失败后立即告知用户'正在重试(1/3)'"后,放弃率从 15% 降到了 5%。沉默不是金,让用户知道系统在挣扎,他们更愿意等。
四、置信度展示:不欺骗用户的信任
AI 最危险的问题不是"错了",而是"错了但看起来很对"。我们在 CLI 输出里实验了三种置信度展示方式:
/// 置信度感知的输出格式化器
/// 根据模型返回的置信度自动调整输出样式
struct ConfidenceAwareFormatter;
impl ConfidenceAwareFormatter {
fn format_output(content: &str, confidence: f64) -> String {
match confidence {
// 高置信度:直接输出
c if c > 0.9 => format!("{}", content),
// 中置信度:标注提醒
c if c > 0.6 => format!(
"⚠️ 置信度: {:.0}% —— 建议人工审核以下内容:\n\n{}",
c * 100.0,
content
),
// 低置信度:修改语气为"建议"
_ => format!(
"💡 以下为参考建议(置信度 {:.0}%):\n\n{}",
confidence * 100.0,
content.replace("你应当", "你可以考虑")
.replace("必须", "建议")
),
}
}
}
/// 置信度阈值的实际调优数据
// ============================================================
// 阈值设太低(<60%),用户盲目信任错误输出 → 投诉率 18%
// 阈值设太高(>95%),几乎所有输出都带警告 → 用户无视警告,投诉率 15%
// 当前方案(60%-90% 分段展示)→ 投诉率 8%
// 关键洞察:用户对"明确说低置信度"的容忍度,远高于"看起来自信但实际错了"
上线后我们还发现了一个反直觉的数据:加了置信度提示后,用户"复制 AI 输出"的比例下降了 26%,但"反馈错误"的比例上升了 40%。表面上看用户更不信任 AI 了,实际上是我们把"盲目信任"转化成了"带着批判使用"——这才是产品真正成熟的标志。
我们也试过在输出末尾加一行小字"本回答由 AI 生成,仅供参考",用户反馈说感觉被当傻子。后来改成了具体置信度数值,效果好很多。
五、总结
程序员做 UX 设计有一个天然优势:你不会被"这是技术债""这不优雅"之类的借口绑架。当我在凌晨两点被用户邮件骂"你的工具卡住了"时,我要的只是明天能少一封这样的邮件。
三个最关键的实践:
- 永远不要让用户面对空白——spinner、进度条、阶段提示,甚至"模型正在思考..."都比空白好一万倍;
- 错误提示要给解决方案——"请求失败"是最无用的错误信息,"请检查 API Key 是否过期(设置路径:~/.config/dayuan/config.toml)"才是有价值的;
- 低置信度的时候要大声说出来——比 AI 犯错更可怕的是用户完全信任了一个错误的输出。
如果你也在做 AI 产品,建议每周花一小时翻用户的反馈邮件。产品的最重要 features 往往不是你想出来的,而是用户"骂"出来的。
下一篇预告:Cargo 与 Nix——用声明式构建保证 Rust 项目的完全可复现。
更多推荐


所有评论(0)