1. 总览

用户消息 → 写入 Messages(原始消息库)
       → 回复时取最近若干条作为短期上下文(工作记忆)
       → (后台) ChatHistorySummarizer:把一段对话归到 topic,并最终落库 ChatHistory(中期记忆)
       → (按需) 检索 ChatHistory:把相关摘要注入 prompt,辅助回答
       → (后台) MemoryForgetTask:四阶段删减 ChatHistory(遗忘)
       → (后台) JargonMiner:抽取黑话候选→累计次数/上下文→阈值推断 meaning(黑话词典)
       → (回复前) JargonExplainer:在上下文里匹配黑话→取 meaning→整理成提示注入
       → PersonInfo:用户画像/别名(长期)

2. 短期记忆:原始消息 + 工作记忆

MaiBot 的短期记忆可以理解为:

  • 落库保存:所有用户与机器人消息都会进入消息表(原始对话存档)。
  • 回复时使用:生成回复时,会挑选最近若干条消息拼成上下文,喂给大模型。
  • [它记什么]
    • 原始消息文本(以及平台、时间、发言人等元信息)
  • [它怎么用]
    • 取最近窗口作为 prompt 上下文(具体取多少条/多少 token 由配置与 prompt 构建逻辑决定)

短期记忆的关键作用是“让模型知道当前对话正在发生什么”,同时它也是后面中期总结、黑话抽取的原材料来源。


3. 中期记忆:把一段对话压缩成可检索的 ChatHistory

3.1 “话题检查”:把消息分配到 topic(并复用历史 topic)

触发后它会做的事很像“轻量主题聚类”:

  • [输入给 LLM 的内容]
    • 一段时间窗口内的消息(带编号)
    • existing_topics:历史缓存里的 topic 名称列表
  • [LLM 输出]
    • 多个 { topic, message_indices }(JSON)

随后系统会把同一 topic 的消息文本合并,追加到该 topic 的缓存里。

3.2 “topic 缓存”:先存本地缓存,等稳定了再落库

MaiBot 会在内存/本地缓存中维护一个 topic_cache(topic → 多段文本片段),并维护一个计数器用来判断“这个 topic 是否稳定、是否该打包存储”。

  • [topic 何时会被认为该入库?]
    • 连续 3 次话题检查都没有新增内容no_update_checks >= 3
    • 该 topic 累积片段太多len(item.messages) > 5
      注意这里的 messages 不是“单条消息数”,而是“每次检查合并后的一个片段”,所以它更像 topic 的“片段数”。

3.3 “打包入库”:生成 keywords/summary 并写入 ChatHistory

当 topic 满足打包条件,系统会再次调用 LLM,把这个 topic 的内容压缩成结构化字段并落到数据库 ChatHistory 表里。

  • [落库字段(核心)]
    • theme:主题标题
    • keywords:关键词(JSON 列表)
    • summary:摘要(平文本)
    • original_text:原始内容拼接(用于检索与详情展示)
    • 以及时间范围、参与人 participants

4. 中期记忆检索:只查持久化的 ChatHistory,不查缓存

4.1 触发方式:先让 LLM 决定“要不要检索”

典型链路是:

4.2 匹配规则:在多个字段里找关键词

search_chat_history() 会在下面字段里做匹配:

  • theme
  • keywords
  • summary
  • original_text

匹配还有个“容错全匹配”策略:

  • 关键词数量 <= 2:必须全匹配
  • 关键词数量 > 2:允许只命中 n-1

最终检索到的结果会被整理成可注入的信息,塞回 prompt,让模型“想得起来”。


5. 遗忘机制:四阶段删减 ChatHistory(每 5 分钟执行一次)

遗忘任务 MemoryForgetTask **每 300 秒(5 分钟)**跑一次,分四阶段。每一阶段的共同点是:

  • 只处理特定 forget_times 的记录(表示处于第几阶段)
  • 只处理超过某个时间阈值的记录
  • count 排序后,删除 最高 X% 和最低 X%(两头删掉),留下中间“相对稳”的部分
  • 对剩余记录把 forget_times +1

四阶段参数如下:

  • [阶段1] 超过 30 分钟、forget_times=0:删高/低 25%,剩余标记为 1
  • [阶段2] 超过 8 小时、forget_times=1:删高/低 7%,剩余标记为 2
  • [阶段3] 超过 48 小时、forget_times=2:删高/低 5%,剩余标记为 3
  • [阶段4] 超过 7 天、forget_times=3:删高/低 2%,剩余标记为 4

这里的 count 是“被检索/使用次数”,越常被用到的中期记忆越不容易被彻底淘汰(但也会避免极端头部永远霸榜——因为高端也会被删一部分)。


6. 长期记忆:用户画像(PersonInfo)里最稳定生效的是“别名”


MaiBot 的用户画像落在 PersonInfo(数据库表)里,最常被调用的是:

  • person_name:给用户起的“别名/称呼”
  • nickname / group_nick_name:平台昵称/群昵称
  • [补充] memory_points 虽然有设计,但默认链路里不一定真的会写入(目前没有调用点)

别名会被用于:

  • 对话上下文展示(谁说了什么)
  • 模型在回复中称呼用户(提高陪伴感与一致性)

7. 黑话系统(Jargon):从“未知词条”到“可解释词典”的闭环

7.1 第一阶段:抽取候选词条并累计证据(直接写 DB)

它不是“每条消息都写”,而是聊天触发 + 冷却门禁后,按窗口抽取:

  • 冷却/门槛:约 20s 冷却 + 窗口内消息数达到阈值才会跑
  • LLM 抽取输出[{content, msg_id}]
  • 落库内容(Jargon 表)
    • count:出现次数累计
    • raw_content:该词出现时的上下文段落列表(每次追加、去重)
    • chat_id:出现在哪些 chat(并带计数)
    • 此时 meaning 可能还是空的

7.2 第二阶段:达到阈值后推断 meaning(只做“含义推断/是否黑话判定”)

count 到达一些阈值(代码里是 [2,4,8,12,24,60,100],并用 last_inference_count 防重复)后,会触发推断流程:

  • 基于上下文推断 meaning
  • 只基于词条推断 meaning
  • 比较两者是否相似
    • 相似:说明不依赖上下文 → 可能不是“黑话”(is_jargon=False,meaning 可能清空)
    • 不相似:说明依赖上下文 → 更像“黑话”(is_jargon=True,保留 meaning)

7.3 在回复里怎么用:匹配当前上下文中的黑话并注入解释

使用时不是“分词”,而是“字符串/正则匹配”:

  • 从 DB 取出所有 meaning 非空Jargon
  • 在当前上下文消息合并文本里用 re.search 匹配出现
  • 找到后把 content: meaning 拼起来
  • 再让 LLM 把多条解释整理成一段自然语言(便于注入 prompt)

8. 小结:这套设计的优点与取舍

  • [优点]
    • 成本可控:短期靠上下文窗口,中期把长对话压缩后再检索,减少 token 消耗
    • 可解释ChatHistoryJargon 都是结构化落库,能审计、能调参
    • 会“遗忘”:四阶段策略避免数据库无限膨胀
  • [取舍]
    • 检索偏“字符串规则”:可用,但对语义相似/同义表达的覆盖有限
    • 依赖 LLM 的稳定性:topic 划分、关键词抽取、黑话含义推断都受 prompt 与模型质量影响

9. 口语版

它记忆的话主要分为三个层次:短期、中期以及长期,还有一些额外的黑话系统。

首先是短期。短期就是单纯地存储用户/AI 的原始消息,然后在进行回复的时候,会选择一部分消息注入上下文。

然后是中期记忆。中期记忆主要是存储一段范围内的对话,以及包含 AI 对话的摘要。它的逻辑是这样的:首先是存储部分,它是一个定时任务,每五分钟执行一次,判断当前的数量是否达到了指定值。目前指定的是 80 条消息。

当这个数量达到的时候,它就会把这一段对话历史以及之前已经总结过的一些话题一起传给大模型,然后让它把这段对话里的每句话拆分到不同的主题下面;如果有新的主题就创建新的主题,然后返回。返回之后会累积到内存中:主题为 T,内容为 value(也就是这段内容拼接起来)。然后每次后面还会去判断一个逻辑:看看当前内存中是否已经有一些主题连续三次检查没有新增,或者该主题累计片段超过五个。

这个时候,它就会把这个主题以及它的原始对话再调用一次大模型去生成关键词,提取关键词以及摘要等信息,然后永久存储到数据库中。数据库中包含关键词、摘要,还有原始消息的拼接部分等字段。

然后是检索部分。检索部分是在每次有用户消息进来的时候,都会同步触发一次大模型,针对用户当前发的话和上下文决定是否需要发起一个问题。然后大模型会生成一个问题,我们再根据这个问题拆分成多个词,到数据库中去检索中期记忆。也就是说,它不会去检索缓存里的,只会检索持久化之后的,然后去匹配关键词、摘要、以及原始对话的拼接内容,匹配得到结果后再注入上下文,给生成回复提供更多信息。

然后还有一个遗忘机制,就是中期记忆有一个遗忘过程。它的遗忘过程设定分为四个阶段,规律大体是:阶段是连续的,每个阶段按一定比例删减。

比如第一个阶段,它会删掉 25%:只处理出现在数据库中超过 30 分钟的记忆,并且在这个范围内,把出现次数最高的 25% 和最低的 25% 去除掉。然后第二个阶段就是处理出现在数据库中超过 8 个小时的内容,删掉前后 8%(高频和低频)的内容。他这样的规律就是说逐步保证出现次数居中的内容更容易存下来,并且你存活得越久就越不容易被删除。对,大概就是这样子,这是它的一套机制。

然后还有用户画像这块。长期记忆目前实际发挥作用的,主要是用户的别名。就是说在用户发起消息之后,它也会触发一条线去告诉 AI 现在用已经有的名字,并判断是否需要给用户起一个新的别名;然后听它判断要不要起新的别名,或者依然使用原有的别名。这个别名发挥作用的话,会在上下文中用:原始消息里会标识是哪个用户发的消息,这个时候就会使用他的名字;AI 也可能会用这个别名来称呼用户,并且会提出一些其他小问题。

还有一个就是黑话机制。黑话机制也可以算一个定时任务,定时从批量的上下文里让 AI 判断是否有一些不理解真实含义的词,可能是黑话或者梗之类的。然后这个时候让它输出它不理解的黑话/梗,以及对应的消息位置。刚开始我说先存储到内存中,其实我刚刚说错了:它在从 AI 那边拿到黑话及对应的原始消息之后,是把它持久化到了数据库中。但这个时候并不会被直接使用,而是当这个黑话出现次数大于指定设定值之后,它就会再调用一次大模型,从这个上下文以及这个黑话中总结出这个黑话的意思。对,这就是整个存储流程。

接下来就是检索流程。检索的话,每次回复回来之后,它会把句子进行拆分,然后去匹配:用黑话去匹配这个句子,看是否能匹配出。如果匹配出来的话,就把匹配到的这一项(以及原始内容和它的意思)一起组成提示词,然后发送给 AI,让它了解到这段对话中是有这些梗/黑话的。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐