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) - ResultString, Boxdyn 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(), Boxdyn 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简单 spinner47%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: OptionString, }, /// 3级 - 系统严重模型过载、服务宕机 /// 策略坦诚告知 预计恢复时间 替代方案 SystemCritical { message: String, estimated_recovery: Optionstd::time::Duration, fallback: OptionString, }, } 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 项目的完全可复现。
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) - ResultString, Boxdyn 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(), Boxdyn 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简单 spinner47%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: OptionString, }, /// 3级 - 系统严重模型过载、服务宕机 /// 策略坦诚告知 预计恢复时间 替代方案 SystemCritical { message: String, estimated_recovery: Optionstd::time::Duration, fallback: OptionString, }, } 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 项目的完全可复现。