当前位置: 首页 > news >正文

【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,程序退出");记录系统终止时间和原因

三、改进带来的核心价值

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

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

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

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

代码 

#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 的匹配阈值等。

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

 

http://www.dtcms.com/a/297767.html

相关文章:

  • 【C++进阶】第7课—红黑树
  • 什么是主成分分析法和方差
  • 【神经网络概述】从感知机到深度神经网络(CNN RNN)
  • 高级05-Java NIO:高效处理网络与文件IO
  • 【NLP舆情分析】基于python微博舆情分析可视化系统(flask+pandas+echarts) 视频教程 - 主页-评论用户时间占比环形饼状图实现
  • vbs-实现模拟打开excel和强制计算和保存
  • 7月25日总结
  • Android Kotlin 协程全面指南
  • Thinkphp8 Redis队列与消息队列Queue
  • C#模拟pacs系统接收并解析影像设备数据(DICOM文件解析)
  • Pattern正则表达式知识点
  • 第二十天(正则表达式与功能实际运用)
  • VUE 学习笔记6 vue数据监测原理
  • 设计模式十:单件模式 (Singleton Pattern)
  • 空间信息与数字技术专业能从事什么工作?
  • 【LeetCode数据结构】二叉树的应用(二)——二叉树的前序遍历问题、二叉树的中序遍历问题、二叉树的后序遍历问题详解
  • uniapp创建vue3+ts+pinia+sass项目
  • 2025年RISC-V中国峰会 主要内容
  • 绘图库 Matplotlib Search
  • RISC-V VP、Gem5、Spike
  • 恋爱时间倒计时网页设计与实现方案
  • 借助Aspose.HTML控件,在 Python 中将 SVG 转换为 PDF
  • Vue nextTick
  • 基于超176k铭文数据,谷歌DeepMind发布Aeneas,首次实现古罗马铭文的任意长度修复
  • MySQL存储引擎深度解析与实战指南
  • Java面试题及详细答案120道之(001-020)
  • JAVA_FIFTEEN_异常
  • LeetCode 233:数字 1 的个数
  • Zero-Shot TrackingT0:对象分割+运动感知记——当“切万物”武士学会运动记忆,目标跟踪稳如老狗
  • 力扣面试150题--寻找旋转排序数组中的最小值