前言

很多同学一听到人工智能、聊天AI,第一反应就是大模型、Transformer、神经网络、海量数据集、GPU训练。
包括我最开始也是这么认为的:AI = 写一堆复杂Python代码训练模型。

不一定所有AI都要神经网络,来试试手写一个带记忆的对话机器人
在这里插入图片描述

但还有一条古老、有趣、完全可控的路线:符号主义AI。
不需要深度学习、不需要训练、不需要数据集,依靠「关键词分组(同义词)+ 状态记忆 + 候选回复池」,也能做出一个不死板、有简单记忆能力的对话程序。

它不是通用大模型,理解能力有限,但胜在:
✅ 代码全透明,每一条回复逻辑都能看懂
✅ 上手门槛低,只需要基础C++
✅ 脱离死板的 if(输入=="xxx") 一对一查表
✅ 同一个语义,换不同说法也能识别,并且共享对话记忆

本文就带着大家从零实现这个小Demo,一起感受这种不一样的“人工心智”设计思路。

设计思路说明

先讲清楚核心架构,这是这个小机器人最精髓的地方:

  1. 同义词分组:把表达同一个意思的词语归为一组,映射到同一个话题标识。比如 hello、hi、你好、嗨 都属于打招呼话题。只要命中组内任意一词,都进入同一个状态。
  2. 话题状态记忆:给每个话题维护一个计数器,记录这个话题被用户询问过多少次。
  3. 差异化回复池:同一个话题,第一次触发、多次触发,分别准备多句候选回复,随机抽取,避免每次回答一模一样。
  4. 隔离状态污染:只有本轮输入命中关键词,才更新计数;本轮没有命中任何话题,直接返回听不懂,不会拿历史状态乱拼接回复(修复容易踩坑的bug)。
  5. 多话题同时命中支持:一句话同时命中多个话题时,可以输出混合风格的回复。

重点区分:

  • 传统查表机器人:输入A→固定输出1,一一映射,非常死板
  • 神经网络大模型:依靠数据拟合概率分布,黑盒,需要训练
  • 我们这款符号对话机器人:人工定义语义分组与规则,靠状态叠加产生变化,可解释、可控

完整代码

#include <iostream>
#include <unordered_map>
#include <string>
#include <vector>
#include <random>

using namespace std;

// 判断字符串s是否包含子串sub
bool str_contains(const string& s, const string& sub) {
    return s.find(sub) != string::npos;
}

// 从候选回复数组中随机挑选一条
string random_pick(const vector<string>& arr) {
    static mt19937 rng(random_device{}());
    uniform_int_distribution<int> dist(0, arr.size() - 1);
    return arr[dist(rng)];
}

// 存储各个话题的交互次数(记忆结构体)
struct Data {
    unordered_map<string, int> ask_times = {};
    Data() {
        ask_times["hello"] = 0;
        ask_times["weather"] = 0;
    }
};

Data s;

// ========== 配置区:同义词分组,新增话题只在这里扩展 ==========
unordered_map<string, vector<string>> keyword_group = {
    {"hello", {"hello", "hi", "嗨", "你好"}},
    {"weather", {"weather", "天气", "下雨", "热不热"}}
};

// 检测输入,返回所有命中的话题列表
vector<string> detect_topic(const string& ipt) {
    vector<string> hit_topics;
    for (auto& item : keyword_group) {
        string topic = item.first;
        auto& words = item.second;
        for (auto& w : words) {
            if (str_contains(ipt, w)) {
                hit_topics.push_back(topic);
                break; // 当前话题只要命中一个同义词即可,不再重复检测本组其他词
            }
        }
    }
    return hit_topics;
}

// 根据本轮命中话题 + 历史计数,生成回复
string generate_reply(bool hit_hello, bool hit_weather) {
    // 同时命中多个话题,混合回复
    if (hit_hello && hit_weather) {
        vector<string> mix = {
            "一边打招呼一边问天气呀~",
            "哈喽,你是想问问天气吗?"
        };
        return random_pick(mix);
    }

    // 打招呼话题分支
    if (hit_hello) {
        int cnt = s.ask_times["hello"];
        if (cnt <= 1) {
            vector<string> hello_first = {
                "你好呀,很高兴认识你!",
                "哈喽!很高兴和你聊天~",
                "嗨,你好呀!"
            };
            return random_pick(hello_first);
        } else {
            vector<string> hello_more = {
                "你好呀,怎么又打了一次招呼呀!",
                "又见到你啦,哈喽~",
                "又跟我打招呼啦!"
            };
            return random_pick(hello_first);
        }
    }

    // 天气话题分支
    if (hit_weather) {
        int cnt = s.ask_times["weather"];
        if (cnt <= 1) {
            vector<string> weather_first = {
                "今天的天气看起来还不错哦",
                "你想了解天气吗?",
                "来聊聊天气啦"
            };
            return random_pick(weather_first);
        } else {
            vector<string> weather_more = {
                "又问天气啦,你很关心天气呀",
                "怎么又想知道天气啦?",
                "还在关心天气情况嘛"
            };
            return random_pick(weather_more);
        }
    }

    // 本轮没有命中任何关键词
    return "我还暂时没听明白你的问题";
}

int main() {
    cout << "===== 符号派简易对话机器人 =====" << endl;
    while (true) {
        cout << "\n请输入你想问的内容:" << endl;
        string ipt;
        cin >> ipt;

        auto hit_list = detect_topic(ipt);
        bool hit_hello = false;
        bool hit_weather = false;
        for (auto& t : hit_list) {
            if (t == "hello") hit_hello = true;
            if (t == "weather") hit_weather = true;
        }

        // 仅本轮命中话题才增加计数,避免记忆污染
        if (hit_hello) s.ask_times["hello"]++;
        if (hit_weather) s.ask_times["weather"]++;

        cout << generate_reply(hit_hello, hit_weather) << endl;
    }
    return 0;
}

运行测试示例

===== 符号派简易对话机器人 =====

请输入你想问的内容:
hello
你好呀,很高兴认识你!

请输入你想问的内容:
你好
你好呀,怎么又打了一次招呼呀!

请输入你想问的内容:
weather
你想了解天气吗?

请输入你想问的内容:
天气
又问天气啦,你很关心天气呀

请输入你想问的内容:
aaaa
我还暂时没听明白你的问题

可以看到:
hello 和 你好 属于同一个话题,记忆互通;weather 和 天气 互通。
多次输入同义内容,会切换二次话术;无关输入不会乱触发历史记忆。

优缺点分析(客观看待,不神化)

✅ 优点

  1. 完全不需要机器学习、不需要训练数据集,纯C++规则代码;
  2. 逻辑透明,出问题可以直接定位;
  3. 拓展简单:新增话题只需要在 keyword_group 添加分组,再补充对应的回复数组;
  4. 规避了最简单一对一if查表的死板问题。

❌ 局限(非常重要,要和大模型区分开)

  1. 所有能识别的词、所有回复全部需要人工预先写好,无法自主学习新词、新知识;
  2. 只是简单子串匹配,容易误触发;
  3. 没有复杂语义理解,没法处理开放式、全新的问题;
  4. 记忆只有简单计数器,没有复杂上下文理解(比如“那明天呢”这种承接语句)。

一句话总结:它是一套人工精心设计的状态对话引擎,不是通用人工智能。
它代表的是早期符号AI的思路,现在大量用在游戏NPC、简单客服机器人、交互式脚本里。

后续拓展方向(留给读者动手)

感兴趣的朋友可以在这个基础上继续改造,保持纯规则路线,不用神经网络:

  1. 遗忘机制:设置衰减,多次不聊某个话题,计数慢慢清零,模拟“慢慢忘记”;
  2. 话题权重:一句话命中多个话题时,优先选择权重更高的话题回复;
  3. 上下文承接:单独记录上一轮话题,本轮无匹配关键词时,可以承接上一个话题;
  4. 配置解耦:把关键词、话术、权重放到json/文本配置文件,代码不用改动;
  5. 优化匹配算法:不用简单find子串匹配,做简单分词,减少误命中。

写在最后

很多人接触AI,直接跳进大模型、深度学习的赛道,容易忽略这条古老但有趣的符号主义路线。
智能不一定只有“训练拟合”这一条路。
亲手去设计状态、语义分组、记忆规则,看着程序按照我们设计的逻辑产生不一样的回复,是一种完全不一样的乐趣。

这个小Demo适合编程新手用来拓宽思路:原来聊天机器人,还可以这么做。
如果你有更好的拓展想法,欢迎在评论区一起交流!


Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐