首页/LingChat 0.4.0 更新新系统设计
更新于:2026-03-12

LingChat 0.4.0 全新的系统设计

1. 旧版本的问题总结

在 LingChat 0.3.0 版本中,随着功能的不断迭代,新的功能和需求在传统的数据存储和更新模式下难以进行扩展,存在以下问题:

  1. 永久记忆和存档冲突:启用永久记忆后,存档功能和永久记忆产生的记忆是冲突的,会导致重复记忆,存储不必要消息和超出LLM上下文等诸多问题。
  2. 永久记忆单一全局存储:永久记忆全局存储,每个角色只有一个永久记忆,导致无法存在诸如galgame的分离式记忆,也就是使用存档分离记忆,如果用户的对话不当还会导致永久记忆内容混乱。
  3. 信息存储不够原子,丢失信息: 本 LingChat 项目会对 LLM 生成的句子进行拆解和分析,诞生新的内容如tts语言,翻译等。但是这些信息在存储到存档的时候都会丢失,导致无法进行对话重播或者在读取存档的时候无法看到历史信息。
  4. 剧本模式无法与现有系统共存:之前的剧本模式设计会导致无法使用存档,无法使用永久记忆,主角无法导入,所有设计臃肿的存放在剧本文件中,导致剧本的设计需要极其复杂,难以维护,也难以与新版本的内容适配。
  5. 无法兼容多人物对话:LLM的对话采用原版的 user assistant system模式存储,无法判断xx台词是谁说的,导致无法进行多人物对话的逻辑设计和存储。
  6. 无法无缝从上一次对话继续:之前进入程序,AI无论如何短期记忆都会被清除,即使使用了永久记忆。而使用存档又无法保持永久记忆功能,导致无法无缝从上一次对话继续。用户必须选择RAG,然后丢失短期记忆,或者自己主动选择存档,然后丢失(或干扰)永久记忆。
  7. 载入存档只载入记忆,丢失其他信息:载入存档只会载入角色记忆,而比如现在舞台上角色的位置,状态,当前对话角色,当前对话背景,BGM,背景特效等信息全部丢失。
  8. 剧本模式只能使用剧本内的角色:剧本模式只能使用预定好的角色,而无法把剧本的内容与一直在养的AI,比如聊了很久的钦灵一起冒险使用。比如经历樱花展,一起在RPG游戏中玩耍等。大大限制了剧本模式的价值和扩展性。

2. 新版本是如何解决这些问题的

综合考虑以上问题,其本质在于对数据的读写上,之前的设计不够原子。在模块间交互的时候,缺少关键的信息导致子系统间无法进行有效的交互。因此,我们决定对程序的数据结构进行重构,以解决这些问题。

2.1. 新的整体游戏存储结构关键设计

针对以上问题,新的架构设计了一个新的整体游戏存储结构,它主要由以下几个重要部分组成:

  1. 游戏角色GameRole: 这里存储了每个角色的信息,包括display_namerole_id或者script_role_idmemory,memory_bank,等。记录每个角色的唯一标识符,以及每个角色持有的记忆。其中如果是role_id则代表这是一个标准游玩角色,如果是script_role_id则代表这是一个剧本角色,通常是NPC。
  2. 游戏状态GameStatus:它用于存储当前游戏的所有状态,包括背景,音乐,特效,当前对话角色current_role等关键信息。相当于之前剧本模式的设计中和风雪提到的GameContext游戏上下文设计。不过这里的设计更加灵活。它还存储了一个最重要的概念:台词表line_list
  3. 台词表line_list:List[LineBase]: 台词表是记忆的原子存储方式,在台词表中会记录ai与玩家之间的所有信息以及信息本身的所有属性,诸如tts音频路径,台词的发言者等。每个ai的memory(也就是LLM使用的记忆列表)都是通过台词表中的信息来构建的。这样设计的好处是,可以保证记忆的原子性,即每个角色的记忆都是独立的,不会互相干扰,可以根据台词表生成专属于自己的记忆。这样设计的好处也在于给LLM最后的memory是完全标准的user+assistant格式,保证LLM回复的稳定性。
  4. 剧本状态ScriptStatus: 在之前的GameContext设计中,剧本的存储状态是完全独立于原来的自由对话模式的,而现在我们就可以把所有的台词都交给主游戏状态,在ScriptStatus专门的存储剧本状态,如当前运行的剧本,当前进行到的章节,事件,以及最重要的剧本变量variables。通过这样分离剧本状态和主游戏状态,我们就可以在自由对话模式中无缝切换到剧本模式,而剧本模式又可以只专注管理剧本专有的状态。

2.2. 以上设计如何解决之前的问题

  1. 存档本身携带永久记忆属性GameRole中的memory_bank即是永久记忆的存储。当为ai构建记忆的时候,只需要从memory_bank中取出对应的记忆,并和台词表结合即可。而GameStatus持有GameRole对象(通过包含对象RoleManager管理),也就是存档记录了每个角色的永久记忆内容,同时可以通过管理获取的台词表的最近X条内容,于短期记忆结合。也可以总结后X条内容,来更新新的永久记忆内容(即更新memory_bank),
  2. 永久记忆可以存在在任何存档中分离独立:永久记忆不再会被全局干扰,而是只有当玩家载入对应的存档的时候才会进行永久记忆的读写。通过对存档的增删改查,可以方便的进行对任意角色在任意存档中的永久记忆的管理。
  3. 信息的存储变的原子且所有内容可用于数据库:现在的台词,不仅可以在读取存档后,在前端自动同步并展示历史信息,并且可以随时进行信息的语音回放,甚至可以从任何节点进行对话的跳转。所有在程序运行中生产的信息都会被存储并允许保存到数据库中,实现诸如语音收藏,自动清理多余音频文件,对话重新生成,多分支对话等高级功能。
  4. 剧本模式作为现有系统的子部分:如先前的设计所言,剧本模式依赖于主游戏状态GameStatus,本身存储剧本的额外变量,进度信息于ScriptStatus中。由于GameStatus本身就支持多角色对话(包括旁白,NPC的介入),所以剧本模式需要做的就只是通过events向前端发送信息,更新台词表,更新游戏状态和剧本变量即可。之前GameContext管理的诸如角色,玩家信息全部交给GameStatus管理。
  5. 多人物对话在任何场景可用:可以随时添加旁白,NPC或者其他角色的对话,由于所有角色从台词表都是构建一个独属于自己的记忆,所以不会出现认不对人,一个LLM维护多个人设等不优雅的设计。
  6. 可以直接从上次对话开始:无论是自由对话,还是已经导入了剧本,无论有没有启用永久记忆,现在所有的逻辑都封装到存档的概念中。只要导入存档,所有的信息都会同步并还原。
  7. 载入存档保存了所有信息:现在载入存档可以完美还原对话状态,包括背景,音乐,角色状态,甚至可以自动导入永久记忆和剧本的进度。
  8. 剧本模式现在允许主角参与:现在你可以让任何角色参与剧本进行冒险,并且与NPC和旁白故事进行互动,当然剧本也可以只讲NPC的故事,而完全不影响主角。

2.3. 在数据库中如何进行存储

详见如下 ER 图:

2.4. 关键流程讲解

1. 台词表的生命周期

  1. 台词表的生命周期从GameStatus的初始化开始,在GameStatus的初始化过程中,会根据角色和剧本的配置,创建对应的台词表。
  2. import_settings的时候会初始化台词表的第一句,也就是主角的system设定消息。
  3. init_lines()存储了台词表初始化的流程,这个过程会自动更新台词表的主角,通过self.game_status.role_managerrefresh_memories_from_lines()方法,自动更新台词表出现的所有角色生成GameRole对象,为它们生成自己对应的记忆。

    这里为他们生成自己的记忆,就是永久记忆可以扩展的地方了,可以通过获取的记忆更新永久记忆,或者把永久记忆添加到记忆中,其中永久记忆使用json格式存储。 @风雪

  4. 台词表到记忆的更新方法是通过MemoryBuilder里面的算法实现的,具体实现流程可以参考RoleManagerMemoryBuilder模块的代码,算法设计解释可以参考我的博客:消息构建器算法设计。时间复杂度是O(N+L)N是台词数量,L是台词长度。
  5. 当调用LLM生成信息的时候,也就是MessageGeneratorprocess_message_stream方法被调用的时候,首先会根据用户输入消息user_message构建用户台词,然后使用台词表构建角色记忆,将对应角色(也就是当前GameStatuscurrent_role)记忆传输到LLM接口中,等待LLM生成消息,把生成出的消息通过程序处理后,更新到台词表中。

2. 剧本模式的运行逻辑(尚未完工)

  1. ScriptManager传输并获取GameStatus,更新GameStatus内部的ScriptStatus,并把这些属性传输给EventsHandlers内,这样每个剧本的事件就可以操作里面的内容进行数据的更新和使用了,可以通过这种方式更新游戏状态,和剧本变量信息。
  2. 玩家保存或加载存档的时候,直接通过获取GameStatus的信息即可。获取当前角色,游戏状态,剧本变量,章节名,事件序列号就可以了。

3. 永久记忆的增删改查逻辑(风雪去康)

  1. 永久记忆(记忆仓库)作为json数据,在数据的读写中是与角色id绑定的(可以是游戏角色,也可以是NPC角色)
  2. 永久记忆的创建和更新,可以通过检查台词表的长度,比如台词表超过200条了,则获取前面的100条传输给专门的LLM Prompts中进行永久记忆的构建和更新,将LLM的输出作为新的永久记忆内容,并保存到json格式中。
  3. 永久记忆的删除,直接删除GameRolememory_bank就完事了。
  4. 永久记忆的查询,直接通过GameRolememory_bank获取即可。关键在于插在memory记忆的哪里比较好,怎么插,prompt怎么写。(可以专门用一个模块管理)
  5. 对于永久记忆和数据库的联动,只需要通过game_database模块里的MemoryManager里提供的增删改查的方法即可(目前没有写完整,只有增删查,而且只有role_id没有script_role_id,等待扩展ing)