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 项目的完全可复现。