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

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

一、代码整体背景

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

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

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

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

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

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

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

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

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

cpp 复制代码
// 获取当前时间字符串(格式: 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()

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

cpp 复制代码
// 写入日志信息
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. 审计与合规

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

代码

cpp 复制代码
#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 的匹配阈值等。

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