让你MUD中的NPC轻松接入人工智能

前言

这两年最火的是什么?当然是大模型,人工智能划时代的突破。MUD游戏怎么充分利用人工智能呢?大模型是生成文本内容,而MUD游戏也是纯文字的,天然的契合,可玩的创意是非常多的,这里只抛转引玉,实现一个让NPC接入人工智能实现智能对话的功能。

示例

先看示例,大家可以直接进我开源的炎黄MUD体验:https://mud.ren/websocket.html 在游戏扬州客栈(新玩家出生地请选扬州)和示例NPC周不通(zhou butong)对话: file file file file file file file

可以看到和NPC可以随意对话,而NPC本身的代码非常简单,一切都是用AI处理的。不仅可以随意闲聊,还可以直接问游戏中的内容,NPC直接调用知识库解答,虽然因为AI本身的问题不完全准确,但大部分内容都是正确的。而已NPC有记忆功能,知道你和他聊过什么,聊游戏无关的内容也会以自己的角色风格回答。

现在把已实现的功能简介如下:

  1. 基于人工智能的对话,可以聊任何内容。
  2. 每个NPC可以基于自己的角色扮演,使用符合身份的语气和玩家沟通。
  3. NPC自动记忆和玩家的聊天记录(下线也没事),而且是永久记忆所有历史对话数据(自动总结对话的历史记忆+详细聊天的最近记忆)。
  4. 实现了基本的好感度功能,和NPC聊天可以增加好感度(你可以继续扩展实现类似燕云十六声中的功能,比如好感度足够高会收到NPC的礼物)
  5. 实现了知识库的功能,允许NPC根据游戏help文档和你对话,避免胡说八道,而且实现了2套知识库,一套是基于关键词搜索的简单向量知识库,一套是基于千问text-embedding-v4向量模型的知识库,优先使用语义理解的向量知识库,如果没配置向量API则转用本地基于关键词的基本知识库。
  6. 向量知识库支持缓存机制,对相同问题会使用内存缓存而不是调千问text-embedding-v4向量模型,提升查询速度降低调用成本。

功能接入

怎么在你的MUD中接入AI功能?好消息是只需三步,对MUD代码几乎无改动,坏消息是只支持utf-8编码的MUD,因为AI响应的是UTF-8数据,我的配置也用的是json文件,老旧MUD赶紧升级吧,不要抱着GBK编码等死。

复制AI功能代码

  1. 把炎黄代码中的ai_service目录复制到你的MUD根目录中(主要是和HELP目录平级就行)
  2. 把炎黄代码中的AI_CLIENT_D守护进程和talk指令复制到你MUD的守护进程和指令目录中

修改NPC代码

在你的NPC中接入以下代码

在现有NPC的create()函数中添加:

set("ai_npc_id", "zhou butong");  // 对应json中的键名,如果不设置则默认npc id

在现有NPC中添加AI对话方法(talk指令调用,你也可以自己修改):

int accept_talk(object me, string topic) {
    string player_id = me->query("id");
    string player_name = me->name();
    string context;

    // 构造简洁的上下文情境信息(可自由灵活配置)
    context = sprintf(
        "时间:%s | 地点:%s | 天气:%s\n"
        "玩家性别:%s | 玩家年龄:%s | 玩家门派:%s | 玩家***:%s",
        NATURE_D->game_time(),
        environment(this_object())->query("short") || "未知",
        trim(remove_ansi(NATURE_D->outdoor_room_description())),
        me->query("gender") || "未知",
        me->query("age") ? sprintf("%d岁", me->query("age")) : "未知",
        me->query("family/family_name") || "无门派",
        me->query("family/master_name") || "无***"
    );

    if (!topic || topic == "") {
        topic = "你好";
    }

    AI_CLIENT_D->send_chat_request(
        query("ai_npc_id") || query("id"),
        player_id,
        player_name,
        topic,
        context
    );

    return 1;
}

这里代码就不用解释了,非常简单,你可以自己进一步改造,其中context部分附加给AI的自定义内容,你可以根据自己的MUD灵活提供,也可以不提供任何内容。

初始化AI数据并启动服务

以下都在ai_service目录下执行:

  • 参考config/npc_roles.json中的NPC配置,加入你的NPC配置,注意键名就是set("ai_npc_id", "zhou butong");指定的名称。
  • 配置大模型环境变量
cd ai_service
echo "OPENAI_API_KEY=你的AI大模型API密钥" > .env
echo "OPENAI_BASE_URL=你的AI大模型API地址" >> .env
echo "OPENAI_MODEL=你的AI大模型名称" >> .env
echo "DASHSCOPE_API_KEY=你的千问API密钥" >> .env
  • 初始化知识库
# 基础关键词搜索
python scripts/setup_basic.py

# 千问向量搜索(推荐)
python scripts/setup_qwen.py

运行脚本会自动读取help目录下所有文本文档生成向量知识库,如果你要更多内容就自己丰富你的HELP文档,然后删除data/basic_knowledge.dbqwen_knowledge.db重新运行脚本。

示例项目使用了最简单的实现,只通过 SQLite+NumPy 的组合轻量化的向量数据库,但对MUD项目完全满足需求。

  • 启动AI服务
python main.py          # 正常模式
python main.py -d       # 调试模式(显示详细日志)

现在你在游戏中就可以和NPC交谈(talk)了。试试吧~

提示:请根据你自己游戏特色修改ai_service\src\npc_manager.py中系统提示词游戏世界背景资料。


如果你对相关知识感兴趣,请继续看下面的内容

知识讲解

方案说明

从接入可看到对MUD的改动非常少,核心功能是独立封装的ai_service服务,为什么这样做,因为是这我个人认为MUD中接入AI的最佳方案,纯在MUD中接入AI也可以,但是需要自己写的代码就多了,而如果你让AI帮你写你会崩溃的,因为AI对LPC语言并不专业,就一个变量声明必须符合c89规范变量声明规则:必须在作用域开头声明变量,不能在代码中间声明变量。 AI都能一直犯错,写在提示词中让它记住也没用。而AI非常擅长Python和Javascript,用这二门语言写AI相关的功能也非常简单,所以你完全可以用AI用这二门语言实现AI功能,并用socket接口和MUD互通。不管是用TCP、UDP还是HTTP通讯都可以。

相关教程:LPC与Python使用SOCKET(HTTP模块)通信教程

MUD关键代码

本地服务使用udp通讯是更简单的,所以我这里AI提供的是UDP服务器 (udp_server.py): 监听127.0.0.1:9999,处理游戏客户端请求。而MUD游戏以客户端的身份和UDP服务请求数据,这个客户端以守护进程ai_client_d.c的方式提供服务。其中的代码也非常简单

void create() {
    // 创建UDP socket
    socket_fd = socket_create(DATAGRAM, "read_callback");
    if (socket_fd < 0) {
        debug_message("AI客户端守护程序: 无法创建socket - " + socket_error(socket_fd));
        return;
    }

    // 绑定到任意端口
    if (socket_bind(socket_fd, 0) < 0) {
        debug_message("AI客户端守护程序: 无法绑定socket");
        socket_close(socket_fd);
        socket_fd = -1;
        return;
    }
}

// 数据接收回调
void read_callback(int fd, mixed message, string addr) {
    mapping response;
    string npc_id, player_id, ai_response;
    object npc, player;

    catch {
        response = json_decode(message);
    };

    if (!response || !mapp(response)) {
        debug_message("AI客户端守护程序: 空或无效响应");
        return;
    }

    npc_id = response["npc_id"];
    player_id = response["player_id"];
    ai_response = response["response"];

    // 处理响应
    if (!npc_id || !player_id || !ai_response)
        return;

    npc = find_living(npc_id);
    player = find_player(player_id);
    if (npc && player) {
        // 换行输出AI响应内容给玩家
        tell_object(player, sort_string(ai_response, 78));
    }
}

// 发送对话请求
varargs string send_chat_request(string npc_id, string player_id, string player_name, string message, string context) {
    mapping request = ([
        "type": "chat",
        "npc_id": npc_id,
        "player_id": player_id,
        "player_name": player_name,
        "message": message,
        "context": context || "无内容"
    ]);
    string json_str = json_encode(request);
    int result;

    if (socket_fd < 0) {
        debug_message("AI客户端守护程序: socket未初始化");
        return "AI服务未启动";
    }

    result = socket_write(socket_fd, json_str, AI_SERVER_HOST + " " + AI_SERVER_PORT);

    if (result < 0) {
        debug_message("AI客户端守护程序: 发送失败 - " + socket_error(result));
        return "AI服务通信异常";
    }

    return "处理中...";
}

通过socket_create(DATAGRAM, "read_callback");创建socket客户端,指定read_callback回调接收AI发过来的消息,使用send_chat_request(string npc_id, string player_id, string player_name, string message, mapping context)发送消息给UDP服务器。

这里需要强调的是MUD中一定要socket_bind(socket_fd, 0);,熟悉socket接口的朋友都知道,UDP通信只需要服务端绑定端口接收消息,但是客户端如果不绑定端口只能发消息给服务端,收不到服务端发回来的消息,所以这里我们用socket_bind(socket_fd, 0);让系统自动选择一个可用端口,保证read_callback回调生效。

socket基础教程:LPC 语言基础教程:9.3 socket 接口介绍及游戏开发实战

一切就是这么简单~你学会了吧?

AI服务端

而关于ai服务的相关代码,是python的语法知识了,这里不过多介绍,只简单说明

  • udp_server.py - 网络网关:接收游戏客户端请求,返回AI回复
  • npc_manager.py - AI大脑:加载配置、生成回复、管理记忆
  • memory_store.py - 长期记忆:存储NPC对玩家的关系数据
  • history_manager.py - 对话记录:永久保存所有聊天记录,查询时返回最近指定数量作为上下文
  • knowledge_qwen.py - 语义搜索:向量知识检索(千问)
  • knowledge_basic.py - 关键词搜索:基础文本匹配回退方案

如果你对具体实现感兴趣,可以自己问AI,最简单的是用豆包,选AI编程,把炎黄github代码提供给它 file 然后有啥不懂的直接点开文件问豆包 file

需要说明的是NPC配置文件

{
  "huang rong": {
    "name": "黄蓉",
    "title": "丐帮帮主",
    "role": "女侠",
    "personality": "聪明伶俐,古灵精怪,爱憎分明",
    "background": "东邪黄药师之女,丐帮帮主,精通厨艺和打狗棒法。",
    "greeting": "这位公子,要不要尝尝我亲手做的叫花鸡?",
    "topics": ["美食", "武功", "江湖", "爱情", "厨艺"],
    "speech_style": "活泼俏皮,机智幽默",
    "knowledge_base": ["烹饪", "武功", "江湖秘闻", "奇门遁甲"],
    "knowledge_threshold": 0.6,
    "memory_capacity": 80,
    "relationship_tips": {
      "gifts": ["美食", "新奇玩意", "珍贵食材"],
      "topics": ["各地美食", "江湖趣事", "爱情故事"],
      "taboos": ["虚伪", "欺负弱小", "不讲义气"]
    }
}

这里设置了NPC的身份,用于系统提示词,具体使用参考npc_manager.py代码:


        # 构建系统提示(只有静态内容,用于缓存命中优化)
        system_prompt = f"""# 角色设定

## 基本信息
**姓名**: {npc_config['name']}
**身份**: {npc_config['title']}
**角色**: {npc_config['role']}

## 人物性格
- **性格特征**: {npc_config['personality']}
- **背景故事**: {npc_config['background']}
- **说话风格**: {npc_config['speech_style']}
- **初次问候**: {npc_config['greeting']}

### 📚 人物专属知识
{person_knowledge}

## 互动偏好
| 类别 | 内容 |
|------|------|
| **擅长话题** | {', '.join(npc_config['topics'])} |
| **喜好礼物** | {', '.join(npc_config.get('relationship_tips', {}).get('gifts', []))} |
| **喜欢话题** | {', '.join(npc_config.get('relationship_tips', {}).get('topics', []))} |
| **忌讳话题** | {', '.join(npc_config.get('relationship_tips', {}).get('taboos', []))} |

**表达要求**:
- **角色化**: 始终用{npc_config['name']}的身份和语气讲述
- **流畅叙述**: 用流畅的口语化叙述表达,避免生硬列表
- **准确表述**: 准确使用参考知识中具体门派、人物、地点、技能名称等内容
- **颜色运用**: 在关键门派、武功、指令等处可巧妙使用ANSI颜色增强可读性
"""

其中knowledge_base可以提供更详细的内容做NPC本人特有的知识,这里只是简单的示例,实际上你可以自己写更详细的专属知识内容,NPC会利用这些知识来解答问题。

knowledge_threshold影响的不是这个基本知识,而是从向量知识库中查找的命中阈值,比如如果你不希望这个NPC用向量知识库中的内容回答玩家,把这个阈值改到1,这样就不会用到向量知识库了。

另外memory_capacity用来指定这个NPC记忆容量,就是记忆和玩家的多少条对话内容,配置少可减少token消耗,记忆容量外的更多历史对话NPC只能记忆概要,如果配置为0代表无记忆的单轮对话。

关于向量知识库的知识,如果你感兴趣,可以看:文本嵌入(Embedding)技术完全教程

有问题请在本贴留言,有任何好的创意想法也可以在此交流。

京ICP备13031296号-4