【c++】问答系统代码改进解析:新增日志系统提升可维护性——关于我用AI编写了一个聊天机器人……(14)
在软件开发中,代码的迭代优化往往从提升可维护性、可追踪性入手。本文将详细解析新增的日志系统改进,以及这些改进如何提升系统的实用性和可调试性。
一、代码整体背景
代码实现了一个基于 TF-IDF 算法的问答系统,核心功能包括:
- 加载训练数据(
training_data.txt
)构建问答库 - 提取中英文关键词(支持 GBK 编码中文处理)
- 通过精确匹配和 TF-IDF 相似度计算返回最佳答案
- 支持基础交互命令(
help
/topics
/exit
等)
其中,改进版在原版本的基础上,重点新增了日志记录功能,下面详细解析具体改进点。
二、核心改进点:新增日志系统
1. 日志相关头文件与常量定义
代码新增了日志功能所需的头文件和常量:
#include <ctime> // 用于日志时间戳
// 日志文件名
const string LOG_FILE = "chat_log.txt";
- 引入
<ctime>
库用于获取当前时间,为日志添加时间戳 - 定义
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,程序退出"); | 记录系统终止时间和原因 |
三、改进带来的核心价值
可追溯性提升
日志记录了系统从启动到退出的全流程状态,以及用户的每一次交互(输入内容、执行命令),当系统出现异常时,可通过日志快速定位问题节点(如数据加载失败、匹配逻辑错误等)。用户行为分析
通过用户输入日志(问题、命令),可以统计高频问题、用户关注的话题等,为优化问答库(补充热门问题答案)提供数据支持。系统调试效率提升
无需通过cout
打印临时调试信息,日志文件可永久保存,便于复现问题和对比不同版本的运行差异。审计与合规
对于需要留存交互记录的场景(如简单的客服系统),日志可作为合规审计的依据。
代码
#include <iostream>
#include <fstream>
#include <string>
#include <map>
#include <vector>
#include <cctype>
#include <cmath>
#include <algorithm>
#include <set>
#include <ctime> // 用于日志时间戳
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 << endl;}
}// 判断是否为中文标点符号(GBK编码)
bool isChinesePunctuation(unsigned char c1, unsigned char c2) {if ((c1 == 0xA1 && (c2 >= 0xA2 && c2 <= 0xAF)) || (c1 == 0xA3 && (c2 == 0xAC || c2 == 0xAD)) || (c1 == 0xBC && (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<unsigned char>(result[i]));}return result;
}// 从字符串中提取关键词(修复中文处理)
vector<string> extractKeywords(const string& text) {vector<string> keywords;string asciiWord; // 存储英文/数字词for (size_t i = 0; i < text.length(); ) {unsigned char c = static_cast<unsigned char>(text[i]);// 处理ASCII字符(0-127)if (c <= 127) {if (isalnum(c)) { // 字母或数字asciiWord += text[i];++i;} else { // ASCII标点或空格,作为分隔符if (!asciiWord.empty()) {keywords.push_back(toLower(asciiWord));asciiWord.clear();}++i;}} // 处理中文字符(GBK编码,2字节)else {if (i + 1 >= text.length()) {++i;continue;}unsigned char c2 = static_cast<unsigned char>(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<string, string>& exactAnswers) {if (exactAnswers.empty()) {cout << "暂无可用的话题信息" << endl;return;}cout << "\n===== 我可以回答这些类型的问题 =====" << endl;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) {map<string, double> userTFIDF;for (vector<string>::const_iterator kit = userKeywords.begin(); kit != userKeywords.end(); ++kit) {const string& keyword = *kit;double tf = 0.0;for (vector<string>::const_iterator it = userKeywords.begin(); it != userKeywords.end(); ++it) {if (*it == keyword) tf++;}tf /= userKeywords.size();double idf = 0.0;map<string, double>::const_iterator idfIt = idfValues.find(keyword);if (idfIt != idfValues.end()) {idf = idfIt->second;}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;map<string, double> questionTFIDF;for (vector<string>::const_iterator kit = keywords.begin(); kit != keywords.end(); ++kit) {const string& keyword = *kit;double tf = 0.0;for (vector<string>::const_iterator it = keywords.begin(); it != keywords.end(); ++it) {if (*it == keyword) tf++;}tf /= keywords.size();double idf = 0.0;map<string, double>::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<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) {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<string, double>::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<string> >::const_iterator ansIt = qas.find(bestQuestion);if (ansIt != qas.end() && !ansIt->second.empty()) {return ansIt->second[0];}}return "";
}int main() {// 初始化日志writeLog("系统", "程序启动");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;}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<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;// 记录训练数据加载情况char logMsg[100];sprintf(logMsg, "加载训练数据完成,共%d条记录", exactAnswers.size());writeLog("系统", logMsg);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;double idf = log(static_cast<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;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<string> inputKeywords = extractKeywords(input);inputClean = "";for (vector<string>::const_iterator it = inputKeywords.begin(); it != inputKeywords.end(); ++it) {inputClean += *it;}bool exactFound = false;for (map<string, string>::const_iterator it = exactAnswers.begin(); it != exactAnswers.end(); ++it) {string questionClean = "";vector<string> qKeywords = extractKeywords(it->first);for (vector<string>::const_iterator qit = qKeywords.begin(); qit != qKeywords.end(); ++qit) {questionClean += *qit;}if (questionClean == inputClean) {cout << "机器人: " << it->second << endl;writeLog("系统响应", "精确匹配回答: " + it->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 的匹配阈值等。
注:本文使用豆包辅助编写