75、C++项目实战:聊天机器人(规则匹配)
说实话,聊天机器人这个项目,是我当年刚学C++时最兴奋的一个实战。你想想看,自己写的程序能跟人对话,哪怕只是最简单的规则匹配,那种成就感也特别真实。今天我们就来做一个基于规则匹配的聊天机器人,不涉及AI、不涉及神经网络,纯粹用C++的字符串处理和逻辑判断来实现。
项目背景与目标
这个项目的核心思路很简单:用户输入一句话,程序根据预设的规则库,找到最匹配的回复。说白了,就是“如果用户说了A,我就回复B”。但实际做起来,你会发现很多细节需要处理。
我记得第一次做这个项目时,我天真地以为用一堆if-else就能搞定。结果用户输入了“你好”、“你好呀”、“你好啊”、“你好吗”……我那个if-else链写得跟面条一样。后来我才意识到,规则匹配的核心在于模式抽象。
项目目标:构建一个支持关键词匹配、模式替换、上下文记忆的轻量级聊天机器人。
整体架构设计
我们先看看这个机器人的整体结构。我习惯把这类项目分成三层:输入层、匹配引擎层、回复生成层。下面这张图能帮你快速理解。
规则匹配的核心实现
匹配引擎是整个机器人的心脏。我把它设计成三个层次:
- 精确匹配:用户输入完全等于某个预设句子
- 关键词匹配:用户输入包含某个关键词
- 模式匹配:用通配符或正则表达式匹配一类模式
实际项目中,用得最多的是关键词匹配和模式匹配。精确匹配太死板,用户稍微换个说法就匹配不上了。
代码实现
我们先定义规则的数据结构。我个人习惯用结构体来组织,清晰明了。
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <cctype>
// 规则结构体
struct Rule {
std::string pattern; // 匹配模式
std::string response; // 回复内容
int priority; // 优先级,数值越大越优先
bool isRegex; // 是否使用正则匹配
};
// 聊天机器人核心类
class ChatBot {
private:
std::vector<Rule> rules;
std::string lastInput; // 记录上一次输入,用于上下文
// 工具函数:转小写
std::string toLower(const std::string& str) {
std::string result = str;
std::transform(result.begin(), result.end(), result.begin(),
[](unsigned char c){ return std::tolower(c); });
return result;
}
// 关键词匹配
bool matchKeyword(const std::string& input, const std::string& keyword) {
return input.find(toLower(keyword)) != std::string::npos;
}
public:
ChatBot() {
// 初始化规则库
initRules();
}
void initRules() {
// 优先级从高到低排列
rules.push_back({"exit", "再见!期待下次聊天。", 10, false});
rules.push_back({"你好", "你好呀!有什么可以帮你的?", 8, false});
rules.push_back({"名字", "我叫小蓝,是你的聊天助手。", 7, false});
rules.push_back({"天气", "今天天气不错,适合出去走走。", 6, false});
rules.push_back({"心情", "我只是一段代码,但希望你能开心!", 5, false});
rules.push_back({"?", "这是个好问题,让我想想……", 4, false});
rules.push_back({"默认", "嗯,我还没学会这个。你能换个说法吗?", 1, false});
}
std::string getResponse(const std::string& input) {
std::string processed = toLower(input);
// 按优先级遍历规则
for (const auto& rule : rules) {
if (rule.pattern == "默认") {
continue; // 默认规则最后处理
}
if (matchKeyword(processed, rule.pattern)) {
lastInput = input;
return rule.response;
}
}
// 没有匹配到任何规则,返回默认回复
lastInput = input;
return rules.back().response;
}
};
小技巧:规则库的优先级设计很重要。我曾经踩过一个坑——把“默认”规则放在最前面,结果所有输入都匹配到了默认回复。后来我改成按优先级降序排列,高优先级的规则先匹配,这样才正常。
上下文记忆的实现
光有单轮对话太单调了。我加了一个简单的上下文记忆功能。比如用户说“我叫张三”,机器人记住名字,下次用户说“我叫什么”,机器人能回答“你叫张三”。
class ContextAwareBot : public ChatBot {
private:
std::map<std::string, std::string> context;
public:
std::string getResponse(const std::string& input) {
std::string processed = toLower(input);
// 提取名字信息
if (processed.find("我叫") != std::string::npos) {
size_t pos = processed.find("我叫") + 4;
std::string name = input.substr(pos);
// 去掉可能的标点
name.erase(std::remove_if(name.begin(), name.end(), ::ispunct), name.end());
context["name"] = name;
return "你好," + name + "!很高兴认识你。";
}
// 查询名字
if (processed.find("我叫什么") != std::string::npos) {
if (context.find("name") != context.end()) {
return "你叫" + context["name"] + "呀,我记着呢!";
} else {
return "你还没告诉我你的名字呢。";
}
}
// 其他情况调用父类匹配
return ChatBot::getResponse(input);
}
};
注意:上下文记忆要小心内存泄漏。虽然这个例子用map没问题,但如果是长期运行的服务器程序,建议加一个上下文过期机制。我曾经见过一个聊天机器人跑了一周,context里存了几十万条记录,内存直接爆了。
规则库的扩展设计
实际项目中,规则库不可能写死在代码里。我建议用配置文件来管理。下面是一个简单的JSON格式规则示例:
// 假设从config.json读取规则
// 格式示例:
[
{"pattern": "你好", "response": "你好呀!", "priority": 8},
{"pattern": "再见", "response": "再见!", "priority": 10},
{"pattern": "你是谁", "response": "我是小蓝机器人", "priority": 9}
]
// 读取配置的代码片段
void loadRulesFromFile(const std::string& filename) {
// 这里用简单的文本解析,实际项目建议用nlohmann/json库
std::ifstream file(filename);
std::string line;
while (std::getline(file, line)) {
// 解析每一行,提取pattern和response
// 省略具体解析代码
}
}
测试与调试
写完了代码,怎么验证它好不好用?我一般会写一个简单的测试框架:
| 测试用例 | 输入 | 期望输出 | 实际输出 |
|---|---|---|---|
| 基础问候 | 你好 | 你好呀!有什么可以帮你的? | ✅ 匹配 |
| 名字查询 | 你叫什么名字 | 我叫小蓝,是你的聊天助手。 | ✅ 匹配 |
| 上下文记忆 | 我叫张三 → 我叫什么 | 你叫张三呀,我记着呢! | ✅ 匹配 |
| 未知输入 | asdfgh | 嗯,我还没学会这个。 | ✅ 匹配 |
核心要点总结:
- 规则匹配的核心是模式抽象,不要用if-else硬编码
- 优先级设计决定了匹配的准确性
- 上下文记忆让对话更自然,但要注意内存管理
- 规则库应该外部化,方便扩展和维护
嗯,这个项目虽然看起来简单,但里面涉及的设计思想——模式匹配、优先级调度、上下文管理——在很多大型系统中都能看到影子。你把这个项目吃透了,后面做更复杂的自然语言处理项目时,会轻松很多。
公众号:蓝海资料掘金营,微信deep3321