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(())
            }
        }
    }
}

三个原则:

  1. 告诉用户"为什么",而不是只告诉"失败了";
  2. 给用户一个下一步动作,而不是让他自己猜;
  3. 分级展示,不要用同样的严重度呈现"网络抖动重试"和"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 设计有一个天然优势:你不会被"这是技术债""这不优雅"之类的借口绑架。当我在凌晨两点被用户邮件骂"你的工具卡住了"时,我要的只是明天能少一封这样的邮件。

三个最关键的实践:

  1. 永远不要让用户面对空白——spinner、进度条、阶段提示,甚至"模型正在思考..."都比空白好一万倍;
  2. 错误提示要给解决方案——"请求失败"是最无用的错误信息,"请检查 API Key 是否过期(设置路径:~/.config/dayuan/config.toml)"才是有价值的;
  3. 低置信度的时候要大声说出来——比 AI 犯错更可怕的是用户完全信任了一个错误的输出。

如果你也在做 AI 产品,建议每周花一小时翻用户的反馈邮件。产品的最重要 features 往往不是你想出来的,而是用户"骂"出来的。


下一篇预告:Cargo 与 Nix——用声明式构建保证 Rust 项目的完全可复现。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐