> 技术文档 > 【c++】问答系统代码改进解析:新增日志系统提升可维护性——关于我用AI编写了一个聊天机器人……(14)

【c++】问答系统代码改进解析:新增日志系统提升可维护性——关于我用AI编写了一个聊天机器人……(14)

在软件开发中,代码的迭代优化往往从提升可维护性、可追踪性入手。本文将详细解析新增的日志系统改进,以及这些改进如何提升系统的实用性和可调试性。

一、代码整体背景

代码实现了一个基于 TF-IDF 算法的问答系统,核心功能包括:

  • 加载训练数据(training_data.txt)构建问答库
  • 提取中英文关键词(支持 GBK 编码中文处理)
  • 通过精确匹配和 TF-IDF 相似度计算返回最佳答案
  • 支持基础交互命令(help/topics/exit等)

其中,改进版在原版本的基础上,重点新增了日志记录功能,下面详细解析具体改进点。

二、核心改进点:新增日志系统

1. 日志相关头文件与常量定义

代码新增了日志功能所需的头文件和常量:

#include  // 用于日志时间戳// 日志文件名const string LOG_FILE = \"chat_log.txt\";
  • 引入库用于获取当前时间,为日志添加时间戳
  • 定义LOG_FILE常量指定日志文件名(chat_log.txt),便于统一管理日志存储路径

2. 时间戳生成函数:getCurrentTime()

为了让日志具备时间维度的可追溯性,改进版新增了时间戳生成函数:

// 获取当前时间字符串(格式: YYYY-MM-DD HH:MM:SS)string getCurrentTime() { time_t now = time(NULL); struct tm* localTime = localtime(&now); char timeStr[20]; sprintf(timeStr, \"%04d-%02d-%02d %02d:%02d:%02d\", localTime->tm_year + 1900, // 年份转换(tm_year为从1900开始的偏移量) localTime->tm_mon + 1, // 月份转换(0-11 → 1-12) localTime->tm_mday, localTime->tm_hour, localTime->tm_min, localTime->tm_sec); return string(timeStr);}
  • 功能:生成YYYY-MM-DD HH:MM:SS格式的时间字符串,确保日志记录的时间精确到秒
  • 优势:统一的时间格式便于后续日志分析(如按时间筛选用户交互记录)

3. 日志写入函数:writeLog()

新增了日志写入核心函数,负责将信息追加到日志文件:

// 写入日志信息void writeLog(const string& type, const string& content) { ofstream logFile(LOG_FILE.c_str(), ios::app); // 追加模式打开 if (logFile.is_open()) { logFile << \"[\" << getCurrentTime() << \"] [\" << type << \"] \" << content << endl; logFile.close(); } else { cerr << \"警告: 无法打开日志文件 \" << LOG_FILE << endl; }}
  • 关键参数:
    • type:日志类型(如 \"系统\"/\"用户命令\"/\"用户输入\"/\"系统响应\"),用于分类日志
    • content:日志具体内容
  • 实现细节:
    • 使用ios::app模式打开文件,确保新日志追加到文件末尾(不覆盖历史记录)
    • 日志格式:[时间戳] [类型] 内容,结构清晰,便于阅读和解析

4. 关键节点日志记录

改进版在程序运行的关键节点添加了日志记录,覆盖系统生命周期和用户交互的全流程:

场景 日志记录代码 作用 程序启动 writeLog(\"系统\", \"程序启动\"); 记录系统初始化时间,用于排查启动故障 训练数据加载完成 sprintf(logMsg, \"加载训练数据完成,共%d条记录\", exactAnswers.size()); writeLog(\"系统\", logMsg); 记录数据加载结果,验证数据是否正确加载 用户输入命令(help) writeLog(\"用户命令\", \"输入help,查看帮助信息\"); 追踪用户使用帮助命令的行为 用户输入命令(topics) writeLog(\"用户命令\", \"输入topics,查看可回答话题\"); 分析用户对话题的关注度 用户输入空内容 writeLog(\"用户输入\", \"空输入\"); 统计无效输入情况,优化交互提示 用户输入问题 writeLog(\"用户输入\", \"问题: \" + input); 记录用户原始问题,用于后续优化问答库 系统返回答案 writeLog(\"系统响应\", \"精确匹配回答: \" + it->second); 或 writeLog(\"系统响应\", \"TF-IDF匹配回答: \" + bestAnswer); 关联用户问题与系统答案,分析匹配准确性 程序退出 writeLog(\"系统\", \"用户输入exit,程序退出\"); 记录系统终止时间和原因

三、改进带来的核心价值

  1. 可追溯性提升
    日志记录了系统从启动到退出的全流程状态,以及用户的每一次交互(输入内容、执行命令),当系统出现异常时,可通过日志快速定位问题节点(如数据加载失败、匹配逻辑错误等)。

  2. 用户行为分析
    通过用户输入日志(问题、命令),可以统计高频问题、用户关注的话题等,为优化问答库(补充热门问题答案)提供数据支持。

  3. 系统调试效率提升
    无需通过cout打印临时调试信息,日志文件可永久保存,便于复现问题和对比不同版本的运行差异。

  4. 审计与合规
    对于需要留存交互记录的场景(如简单的客服系统),日志可作为合规审计的依据。

代码 

#include #include #include #include #include #include #include #include #include #include  // 用于日志时间戳using namespace std;// 日志文件名const string LOG_FILE = \"chat_log.txt\";// 获取当前时间字符串(格式: YYYY-MM-DD HH:MM:SS)string getCurrentTime() { time_t now = time(NULL); struct tm* localTime = localtime(&now); char timeStr[20]; sprintf(timeStr, \"%04d-%02d-%02d %02d:%02d:%02d\", localTime->tm_year + 1900, localTime->tm_mon + 1, localTime->tm_mday, localTime->tm_hour, localTime->tm_min, localTime->tm_sec); return string(timeStr);}// 写入日志信息void writeLog(const string& type, const string& content) { ofstream logFile(LOG_FILE.c_str(), ios::app); // 追加模式打开 if (logFile.is_open()) { logFile << \"[\" << getCurrentTime() << \"] [\" << type << \"] \" << content << endl; logFile.close(); } else { cerr << \"警告: 无法打开日志文件 \" << LOG_FILE <= 0xA2 && c2 = 0x80 && c2 <= 0x8F))) { return true; } return false;}// 将字符串转换为小写(仅处理ASCII字符)string toLower(const string& str) { string result = str; for (size_t i = 0; i < result.length(); ++i) { result[i] = tolower(static_cast(result[i])); } return result;}// 从字符串中提取关键词(修复中文处理)vector extractKeywords(const string& text) { vector keywords; string asciiWord; // 存储英文/数字词 for (size_t i = 0; i < text.length(); ) { unsigned char c = static_cast(text[i]); // 处理ASCII字符(0-127) if (c = text.length()) { ++i; continue; } unsigned char c2 = static_cast(text[i+1]); // 过滤中文标点 if (isChinesePunctuation(c, c2)) { if (!asciiWord.empty()) {  keywords.push_back(toLower(asciiWord));  asciiWord.clear(); } i += 2; continue; } // 提取单个汉字作为关键词 string chineseChar; chineseChar += text[i]; chineseChar += text[i+1]; keywords.push_back(chineseChar); i += 2; } } // 处理剩余的ASCII词 if (!asciiWord.empty()) { keywords.push_back(toLower(asciiWord)); } 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& exactAnswers) { if (exactAnswers.empty()) { cout << \"暂无可用的话题信息\" << endl; return; } cout << \"\\n===== 我可以回答这些类型的问题 =====\" << endl; int count = 0; for (map::const_iterator it = exactAnswers.begin(); it != exactAnswers.end() && count first; if (sample.length() > 30) { sample = sample.substr(0, 30) + \"...\"; } cout << \"- \" << sample < 5) { cout << \"... 还有 \" << (exactAnswers.size() - 5) << \" 个其他话题\" << endl; } cout << \"=================================\\n\" << endl;}// 计算TF-IDF并返回最佳匹配答案string getBestAnswerByTFIDF( const vector& userKeywords, const map<string, vector >& qas, const map<string, vector >& questionKeywords, const map& idfValues) { map userTFIDF; for (vector::const_iterator kit = userKeywords.begin(); kit != userKeywords.end(); ++kit) { const string& keyword = *kit; double tf = 0.0; for (vector::const_iterator it = userKeywords.begin(); it != userKeywords.end(); ++it) { if (*it == keyword) tf++; } tf /= userKeywords.size(); double idf = 0.0; map::const_iterator idfIt = idfValues.find(keyword); if (idfIt != idfValues.end()) { idf = idfIt->second; } userTFIDF[keyword] = tf * idf; } map similarityScores; for (map<string, vector >::const_iterator pit = questionKeywords.begin(); pit != questionKeywords.end(); ++pit) { const string& question = pit->first; const vector& keywords = pit->second; map questionTFIDF; for (vector::const_iterator kit = keywords.begin(); kit != keywords.end(); ++kit) { const string& keyword = *kit; double tf = 0.0; for (vector::const_iterator it = keywords.begin(); it != keywords.end(); ++it) { if (*it == keyword) tf++; } tf /= keywords.size(); double idf = 0.0; map::const_iterator idfIt = idfValues.find(keyword); if (idfIt != idfValues.end()) { idf = idfIt->second; } questionTFIDF[keyword] = tf * idf; } double dotProduct = 0.0; double userNorm = 0.0; double questionNorm = 0.0; for (map::const_iterator uit = userTFIDF.begin(); uit != userTFIDF.end(); ++uit) { const string& keyword = uit->first; double userWeight = uit->second; userNorm += userWeight * userWeight; map::const_iterator qit = questionTFIDF.find(keyword); if (qit != questionTFIDF.end()) { dotProduct += userWeight * qit->second; } } for (map::const_iterator qit = questionTFIDF.begin(); qit != questionTFIDF.end(); ++qit) { questionNorm += qit->second * qit->second; } 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::const_iterator it = similarityScores.begin(); it != similarityScores.end(); ++it) { if (it->second > maxSimilarity) { maxSimilarity = it->second; bestQuestion = it->first; } } if (maxSimilarity >= 0.15) { map<string, vector >::const_iterator ansIt = qas.find(bestQuestion); if (ansIt != qas.end() && !ansIt->second.empty()) { return ansIt->second[0]; } } return \"\";}int main() { // 初始化日志 writeLog(\"系统\", \"程序启动\"); map exactAnswers; map<string, vector > qas; map<string, vector > questionKeywords; map 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; } if (line.size() >= 2 && line.substr(0, 2) == \"Q:\") { question = line.substr(2); readingAnswer = false; totalDocuments++; } else if (line.size() >= 2 && line.substr(0, 2) == \"A:\") { if (!question.empty()) {  string answer = line.substr(2);  exactAnswers[question] = answer;  qas[question].push_back(answer);  vector keywords = extractKeywords(question);  questionKeywords[question] = keywords;  set uniqueKeywords;  for (vector::const_iterator it = keywords.begin(); it != keywords.end(); ++it) { uniqueKeywords.insert(*it);  }  for (set::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; // 记录训练数据加载情况 char logMsg[100]; sprintf(logMsg, \"加载训练数据完成,共%d条记录\", exactAnswers.size()); writeLog(\"系统\", logMsg); map idfValues; for (map::const_iterator it = documentFrequency.begin(); it != documentFrequency.end(); ++it) { const string& keyword = it->first; int df = it->second; double idf = log(static_cast(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; writeLog(\"系统\", \"用户输入exit,程序退出\"); break; } else if (input == \"help\") { showHelp(); writeLog(\"用户命令\", \"输入help,查看帮助信息\"); continue; } else if (input == \"topics\") { showTopics(exactAnswers); writeLog(\"用户命令\", \"输入topics,查看可回答话题\"); continue; } else if (input.empty()) { cout << \"机器人: 您的输入为空,请重新输入\" << endl; writeLog(\"用户输入\", \"空输入\"); continue; } // 记录用户输入 writeLog(\"用户输入\", \"问题: \" + input); // 精确匹配尝试 string inputClean = input; vector inputKeywords = extractKeywords(input); inputClean = \"\"; for (vector::const_iterator it = inputKeywords.begin(); it != inputKeywords.end(); ++it) { inputClean += *it; } bool exactFound = false; for (map::const_iterator it = exactAnswers.begin(); it != exactAnswers.end(); ++it) { string questionClean = \"\"; vector qKeywords = extractKeywords(it->first); for (vector::const_iterator qit = qKeywords.begin(); qit != qKeywords.end(); ++qit) {  questionClean += *qit; } if (questionClean == inputClean) {  cout << \"机器人: \" <second <second);  exactFound = true;  break; } } if (exactFound) { continue; } // 关键词匹配 string bestAnswer = getBestAnswerByTFIDF(inputKeywords, qas, questionKeywords, idfValues); if (!bestAnswer.empty()) { cout << \"机器人: \" << bestAnswer << endl; writeLog(\"系统响应\", \"TF-IDF匹配回答: \" + bestAnswer); continue; } cout << \"机器人: 抱歉,我不太理解这个问题。\" << endl; cout << \"您可以尝试输入 \'topics\' 查看我能回答的问题类型\" << endl; writeLog(\"系统响应\", \"无法匹配到合适回答\"); } } else { cout << \"无法打开训练文件 training_data.txt,请确保文件存在且路径正确\" << endl; writeLog(\"错误\", \"无法打开训练文件 training_data.txt\"); return 1; } return 0;}

四、总结

本次改进的核心是新增了结构化日志系统,通过在关键节点记录时间戳、事件类型和具体内容,显著提升了问答系统的可维护性和可分析性。这种改进思路具有通用性 —— 对于任何需要长期运行或涉及用户交互的程序,添加日志系统都是低成本高收益的优化手段。

 

后续可基于此日志系统进一步扩展,例如:添加日志级别(INFO/WARN/ERROR)、实现日志文件按日期分割(避免单文件过大)、或通过日志分析自动优化 TF-IDF 的匹配阈值等。

注:本文使用豆包辅助编写