本期依舊使用豆包輔助完成代碼。
從功能到體驗的轉變
上個版本已經實現了問答系統的核心功能:基于 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.",沒有提供進一步指導。
改進版則提供了的建議:
cout << "機器人: 抱歉,我不太理解這個問題。" << endl;
cout << "您可以嘗試:" << endl;
cout << "- 用不同的方式表述問題" << endl;
cout << "- 輸入 'topics' 查看我能回答的問題類型" << endl;
cout << "- 輸入 'help' 查看幫助信息" << endl;
這種處理方式不僅告知用戶問題,還提供了解決方案,大大降低了用戶的挫敗感。
5. 錯誤提示
對于文件打開失敗等錯誤情況,改進版提供了更具體的指導:
cout << "無法打開訓練文件 training_data.txt" << endl;
cout << "請確保該文件存在于程序運行目錄下" << endl;
cout << "程序將退出..." << endl;
相比上個版本簡單的錯誤提示,用戶能更清楚地了解問題所在及如何解決。
為什么這些改進很重要?
這些看似細微的變化,實際上對用戶體驗有著顯著影響:
- 降低學習成本:良好的引導讓新用戶能快速上手
- 減少挫敗感:當系統無法回答時,提供建設性建議
- 增強掌控感:豐富的命令系統讓用戶能更好地控制交互過程
- 提升信任度:專業的錯誤處理和提示讓用戶更信任系統能力
在 AI 助手和問答系統日益普及的今天,技術實現固然重要,但能否提供自然、友好的交互體驗往往是決定產品成敗的關鍵因素。
代碼
#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-IDFuserTFIDF[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-IDFquestionTFIDF[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)) + 1double 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;
}
總結
這個案例展示了如何通過關注用戶體驗細節,將一個功能性的程序轉變為一個易用、友好的工具。這些改進不需要復雜的技術實現,卻能顯著提升用戶滿意度。
在實際開發中,我們應該始終記住:代碼是寫給機器執行的,但最終是給人使用的。良好的用戶體驗設計,應該貫穿于軟件開發的每一個環節。