【c++】提升用户体验:问答系统的交互优化实践——关于我用AI编写了一个聊天机器人……(12)

本期依旧使用豆包辅助完成代码。

从功能到体验的转变

上个版本已经实现了问答系统的核心功能:基于 TF-IDF 算法的问题匹配和回答。它能够读取训练数据,处理用户输入,并返回最相关的答案。但在用户体验方面还有很大提升空间。

让我们看看改进版做了哪些关键优化:

1. 引导系统

上个版本仅在启动时显示简单的 "Hello! 输入 'exit' 结束对话。" 提示,对于初次使用的用户来说不够友好。

改进版增加了:

  • 详细的欢迎信息和功能介绍
  • 专门的showHelp()函数,提供完整的使用指南
  • showTopics()函数,展示系统能回答的问题类型示例

这些引导信息让用户能快速了解系统功能和使用方法,减少了使用障碍。

2. 命令系统

上个版本仅支持 "exit" 一个命令,功能单一。

改进版扩展为多个命令:

  • "exit" 或 "quit":退出程序(支持多种退出方式)
  • "help":查看帮助信息
  • "topics":了解系统能回答的问题类型

多样化的命令让用户能更好地掌控交互过程,提升了系统的可用性。

3.交互提示

上个版本使用简单的 "You:" 作为输入提示,显得生硬。

改进版对此进行了全面优化:

  • 输入提示改为更亲切的 "请输入您的问题:"
  • 机器人回复前缀从 "Robot:" 改为 "机器人:",更符合中文语境
  • 增加空输入处理,当用户输入为空时给予明确提示

这些细节变化让整个交互过程更加自然流畅。

4. 智能的错误处理与引导

上个版本在无法回答问题时,仅简单返回 "I don't know how to answer this question.",没有提供进一步指导。

改进版则提供了的建议:

cpp 复制代码
cout << "机器人: 抱歉,我不太理解这个问题。" << endl;
cout << "您可以尝试:" << endl;
cout << "- 用不同的方式表述问题" << endl;
cout << "- 输入 'topics' 查看我能回答的问题类型" << endl;
cout << "- 输入 'help' 查看帮助信息" << endl;

这种处理方式不仅告知用户问题,还提供了解决方案,大大降低了用户的挫败感。

5. 错误提示

对于文件打开失败等错误情况,改进版提供了更具体的指导:

cpp 复制代码
cout << "无法打开训练文件 training_data.txt" << endl;
cout << "请确保该文件存在于程序运行目录下" << endl;
cout << "程序将退出..." << endl;

相比上个版本简单的错误提示,用户能更清楚地了解问题所在及如何解决。

为什么这些改进很重要?

这些看似细微的变化,实际上对用户体验有着显著影响:

  1. 降低学习成本:良好的引导让新用户能快速上手
  2. 减少挫败感:当系统无法回答时,提供建设性建议
  3. 增强掌控感:丰富的命令系统让用户能更好地控制交互过程
  4. 提升信任度:专业的错误处理和提示让用户更信任系统能力

在 AI 助手和问答系统日益普及的今天,技术实现固然重要,但能否提供自然、友好的交互体验往往是决定产品成败的关键因素。

代码

cpp 复制代码
#include <iostream>
#include <fstream>
#include <string>
#include <map>
#include <vector>
#include <cctype>
#include <cmath>
#include <algorithm>
#include <set>
using namespace std;

// 将字符串转换为小写
string toLower(const string& str) {
    string result = str;
    for (string::size_type i = 0; i < result.length(); ++i) {
        result[i] = tolower(result[i]);
    }
    return result;
}

// 从字符串中提取关键词
vector<string> extractKeywords(const string& text) {
    vector<string> keywords;
    string word;
    for (string::const_iterator it = text.begin(); it != text.end(); ++it) {
        if (isalnum(*it)) {
            word += *it;
        } else if (!word.empty()) {
            keywords.push_back(toLower(word));
            word.clear();
        }
    }
    if (!word.empty()) {
        keywords.push_back(toLower(word));
    }
    return keywords;
}

// 显示帮助信息
void showHelp() {
    cout << "\n===== 使用帮助 =====" << endl;
    cout << "1. 直接输入您的问题,我会尽力为您解答" << endl;
    cout << "2. 输入 'exit' 或 'quit' 结束对话" << endl;
    cout << "3. 输入 'help' 查看帮助信息" << endl;
    cout << "4. 输入 'topics' 查看我能回答的问题类型" << endl;
    cout << "====================\n" << endl;
}

// 显示可回答的话题类型
void showTopics(const map<string, string>& exactAnswers) {
    if (exactAnswers.empty()) {
        cout << "暂无可用的话题信息" << endl;
        return;
    }
    
    cout << "\n===== 我可以回答这些类型的问题 =====" << endl;
    
    // 提取部分问题作为示例(最多显示5个)
    int count = 0;
    for (map<string, string>::const_iterator it = exactAnswers.begin(); 
         it != exactAnswers.end() && count < 5; ++it, ++count) {
        string sample = it->first;
        if (sample.length() > 30) {
            sample = sample.substr(0, 30) + "...";
        }
        cout << "- " << sample << endl;
    }
    
    if (exactAnswers.size() > 5) {
        cout << "... 还有 " << (exactAnswers.size() - 5) << " 个其他话题" << endl;
    }
    cout << "=================================\n" << endl;
}

// 计算TF-IDF并返回最佳匹配答案
string getBestAnswerByTFIDF(
    const vector<string>& userKeywords,
    const map<string, vector<string> >& qas,
    const map<string, vector<string> >& questionKeywords,
    const map<string, double>& idfValues) {
    
    // 计算用户问题的TF-IDF向量
    map<string, double> userTFIDF;
    for (vector<string>::const_iterator kit = userKeywords.begin(); kit != userKeywords.end(); ++kit) {
        const string& keyword = *kit;
        // 计算词频(TF)
        double tf = 0.0;
        for (vector<string>::const_iterator it = userKeywords.begin(); it != userKeywords.end(); ++it) {
            if (*it == keyword) tf++;
        }
        tf /= userKeywords.size();
        
        // 获取IDF值
        double idf = 0.0;
        map<string, double>::const_iterator idfIt = idfValues.find(keyword);
        if (idfIt != idfValues.end()) {
            idf = idfIt->second;
        }
        
        // 计算TF-IDF
        userTFIDF[keyword] = tf * idf;
    }
    
    // 计算每个问题的相似度
    map<string, double> similarityScores;
    for (map<string, vector<string> >::const_iterator pit = questionKeywords.begin(); 
         pit != questionKeywords.end(); ++pit) {
        const string& question = pit->first;
        const vector<string>& keywords = pit->second;
        
        // 计算问题的TF-IDF向量
        map<string, double> questionTFIDF;
        for (vector<string>::const_iterator kit = keywords.begin(); kit != keywords.end(); ++kit) {
            const string& keyword = *kit;
            // 计算词频(TF)
            double tf = 0.0;
            for (vector<string>::const_iterator it = keywords.begin(); it != keywords.end(); ++it) {
                if (*it == keyword) tf++;
            }
            tf /= keywords.size();
            
            // 获取IDF值
            double idf = 0.0;
            map<string, double>::const_iterator idfIt = idfValues.find(keyword);
            if (idfIt != idfValues.end()) {
                idf = idfIt->second;
            }
            
            // 计算TF-IDF
            questionTFIDF[keyword] = tf * idf;
        }
        
        // 计算余弦相似度
        double dotProduct = 0.0;
        double userNorm = 0.0;
        double questionNorm = 0.0;
        
        // 计算点积和范数
        for (map<string, double>::const_iterator uit = userTFIDF.begin(); uit != userTFIDF.end(); ++uit) {
            const string& keyword = uit->first;
            double userWeight = uit->second;
            
            userNorm += userWeight * userWeight;
            
            map<string, double>::const_iterator qit = questionTFIDF.find(keyword);
            if (qit != questionTFIDF.end()) {
                dotProduct += userWeight * qit->second;
            }
        }
        
        for (map<string, double>::const_iterator qit = questionTFIDF.begin(); qit != questionTFIDF.end(); ++qit) {
            double questionWeight = qit->second;
            questionNorm += questionWeight * questionWeight;
        }
        
        userNorm = sqrt(userNorm);
        questionNorm = sqrt(questionNorm);
        
        // 计算相似度
        double similarity = 0.0;
        if (userNorm > 0 && questionNorm > 0) {
            similarity = dotProduct / (userNorm * questionNorm);
        }
        
        similarityScores[question] = similarity;
    }
    
    // 找到相似度最高的问题
    string bestQuestion;
    double maxSimilarity = 0.0;
    
    for (map<string, double>::const_iterator it = similarityScores.begin(); it != similarityScores.end(); ++it) {
        if (it->second > maxSimilarity) {
            maxSimilarity = it->second;
            bestQuestion = it->first;
        }
    }
    
    // 如果相似度足够高,返回对应的答案
    if (maxSimilarity >= 0.2) { // 相似度阈值
        map<string, vector<string> >::const_iterator ansIt = qas.find(bestQuestion);
        if (ansIt != qas.end() && !ansIt->second.empty()) {
            return ansIt->second[0]; // 假设第一个答案是最佳答案
        }
    }
    
    return ""; // 没有找到匹配
}

int main() {
    // 存储训练数据
    map<string, string> exactAnswers;       // 精确匹配回答
    map<string, vector<string> > qas;        // 问题-回答映射
    map<string, vector<string> > questionKeywords; // 问题-关键词映射
    map<string, int> documentFrequency;     // 关键词-文档频率映射
    
    // 加载训练数据
    ifstream trainingFile("training_data.txt");
    if (trainingFile.is_open()) {
        string line;
        string question = "";
        bool readingAnswer = false;
        int totalDocuments = 0;
        
        while (getline(trainingFile, line)) {
            // 空行表示一个问答对结束
            if (line.empty()) {
                question = "";
                readingAnswer = false;
                continue;
            }
            
            // 问题行以Q:开头
            if (line.substr(0, 2) == "Q:") {
                question = line.substr(2);
                readingAnswer = false;
                totalDocuments++;
            }
            // 回答行以A:开头
            else if (line.substr(0, 2) == "A:") {
                if (!question.empty()) {
                    string answer = line.substr(2);
                    // 保存精确匹配回答
                    exactAnswers[question] = answer;
                    
                    // 保存问题-回答映射
                    qas[question].push_back(answer);
                    
                    // 提取关键词并保存
                    vector<string> keywords = extractKeywords(question);
                    questionKeywords[question] = keywords;
                    
                    // 更新文档频率
                    set<string> uniqueKeywords;
                    for (vector<string>::const_iterator it = keywords.begin(); it != keywords.end(); ++it) {
                        uniqueKeywords.insert(*it);
                    }
                    for (set<string>::const_iterator it = uniqueKeywords.begin(); it != uniqueKeywords.end(); ++it) {
                        documentFrequency[*it]++;
                    }
                }
                readingAnswer = true;
            }
            // 多行回答的后续行
            else if (readingAnswer && !question.empty()) {
                exactAnswers[question] += "\n" + line;
                qas[question].back() += "\n" + line;
            }
        }
        trainingFile.close();
        cout << "已加载 " << exactAnswers.size() << " 条训练数据" << endl;
        
        // 计算IDF值
        map<string, double> idfValues;
        for (map<string, int>::const_iterator it = documentFrequency.begin(); it != documentFrequency.end(); ++it) {
            const string& keyword = it->first;
            int df = it->second;
            // IDF公式: log(总文档数 / (包含该词的文档数 + 1)) + 1
            double idf = log((double)totalDocuments / (df + 1)) + 1;
            idfValues[keyword] = idf;
        }
        
        // 欢迎信息和初始引导
        cout << "\n=================================" << endl;
        cout << "欢迎使用问答系统!我可以回答您的问题" << endl;
        cout << "输入 'help' 查看可用命令,'exit' 退出程序" << endl;
        cout << "=================================\n" << endl;
        
        // 聊天界面
        string input;
        
        while (true) {
            cout << "请输入您的问题: ";
            getline(cin, input);
            
            // 处理命令
            if (input == "exit" || input == "quit") {
                cout << "机器人: 再见!感谢使用!" << endl;
                break;
            }
            else if (input == "help") {
                showHelp();
                continue;
            }
            else if (input == "topics") {
                showTopics(exactAnswers);
                continue;
            }
            else if (input.empty()) {
                cout << "机器人: 您的输入为空,请重新输入或输入 'help' 查看帮助" << endl;
                continue;
            }
            
            // 精确匹配
            map<string, string>::const_iterator exactIt = exactAnswers.find(input);
            if (exactIt != exactAnswers.end()) {
                cout << "机器人: " << exactIt->second << endl;
                continue;
            }
            
            // 关键词匹配 (TF-IDF)
            vector<string> userKeywords = extractKeywords(input);
            string bestAnswer = getBestAnswerByTFIDF(
                userKeywords, qas, questionKeywords, idfValues);
            
            if (!bestAnswer.empty()) {
                cout << "机器人: " << bestAnswer << endl;
                continue;
            }
            
            // 没有找到匹配,提供引导
            cout << "机器人: 抱歉,我不太理解这个问题。" << endl;
            cout << "您可以尝试:" << endl;
            cout << "- 用不同的方式表述问题" << endl;
            cout << "- 输入 'topics' 查看我能回答的问题类型" << endl;
            cout << "- 输入 'help' 查看帮助信息" << endl;
        }
    } else {
        cout << "无法打开训练文件 training_data.txt" << endl;
        cout << "请确保该文件存在于程序运行目录下" << endl;
        cout << "程序将退出..." << endl;
    }
    
    return 0;
}

总结

这个案例展示了如何通过关注用户体验细节,将一个功能性的程序转变为一个易用、友好的工具。这些改进不需要复杂的技术实现,却能显著提升用户满意度。

在实际开发中,我们应该始终记住:代码是写给机器执行的,但最终是给人使用的。良好的用户体验设计,应该贯穿于软件开发的每一个环节。