大模型天生就有有记忆缺陷
就同HTTP请求一般,每次对大模型的请求,都是无状态的。

所以为了让大模型拥有记忆,就需要在每次请求里面,携带上他之前说过的话。从而模仿他的记忆,让他知道,自己是谁,为谁服务,服务什么,服务对象又有什么性格等等…

但是这就会遇到一个很严肃的问题。大模型的推理文本长度是有限的。
也就是我们常说的上下文。这就牵扯出一个问题:
主流大模型的上下文长度也就1Mtoken左右。而国产大模型通常也就是256K左右。
这就意味着上下文长度是有限的,所以非常宝贵。
同时因为大模型的注意力机制的原因,上下文越长,推理的所需耗费的几何倍上升,并且精度也会越低
所以,记忆模块的设计,就至关重要!

为好奇的小伙伴,补充一下什么是注意力机制

他的核心就是自注意力机制(Transformer),不再像RNN那样串行处理,而是一次性看待整段输入(可以并行处理)。
做法就是,对每个token生成QKV三个向量,计算出所有token的QK相似度得分,拿着这个相似度去和V做一个加权求和。从而能让token和语境关联。
就相当于把苹果手机中的,苹果从原本的向量区间,拉到手机品牌向量区间那样。

一、记忆模块设计

在开始搭建记忆模块之初,最先做的并不应该是RAG!
而是在domain协议层,定下契约。之后不论是写入、召回都会按照固定的格式进行。
通常记忆模块会分四层
感知记忆(当前的输入)、短期记忆(上下文的压缩)、长期记忆(沉淀后的记忆)、结构化记忆(存储的用户的习惯、爱好、目标等)。

而我在设计中,

我对所有的 memory 记忆类型 分为三种:

  • 结构化type。
  • 长期记忆
  • 上下文压缩后的摘要,服务与上下文恢复。

同时,为了保护用户隐私,我又分了三层。

  • self:用户个人隐私,像习惯、爱好、自己的上下文等。
  • org:组织级别的,拥有对应权限的人可以共享。
  • platform:平台运维级别的。

虽然有些功能,暂时不需要实现,但是协议是非常有必要提前定义好的。

二、记忆治理

第一部分,已经定义好类型权限了。
但在入库前,更重要的就是定入库的规则。
我这里分为了两层:

  • 软规则:写进 prompt ,让 LLM 按照给定的规则去"理解和生成"。
  • 硬规则:负责最终"裁决和落库",像权限的过滤,字段的合法性校验等,尽可能的规避LLM生成的错误。

其中,软规则中的prompt更多是在告诉LLM。

  • 什么样的内容算高价值
  • 什么东西不能记
  • Fact、Document、摘要等等,给出来的东西应该是什么类型
  • 生成的是json格式
  • 等等…

硬规则,就是对生成的东西进行一次强校验。
就像权限范围TTL等,都是需要自软规则里面抽取出来,经过一遍硬规则的手。
确保一切没有问题。在入库。

我举个简单的例子:

比如用户说:
“以后请你回答时更简洁一点。我最近 30 天目标是刷 200 道题。”

LLM 可能提两个候选:

[
  {
    "type": "fact",
    "namespace": "user_preference",
    "key": "answer_style",
    "value": {"style": "concise"},
    "source_kind": "explicit_user_statement",
    "scope_hint": "self",
    "confidence": 0.95
  },
  {
	 "type": "fact",
	 "namespace": "oj_goal",
	 "key": "current_goal",
	 "value": {"goal": "200 problems in 30 days"},
	 "source_kind": "explicit_user_statement",
	 "scope_hint": "self",
	 "confidence": 0.91,
	 "ttl_hint": {
	   "mode": "duration",
	   "days": 30,
	   "reason": "用户明确说最近 30 天目标"
	  }
]

  • 先判断一下致信度,太低的话直接skip掉。
  • 然后根据LLM生成的,开始配置一下权限,在过滤一遍TTL等,最后入库。

三、上下文压缩,以及上下文恢复

现在定义了如何记忆类型、以及允许什么样的内容入库。
但是另一个影响对话质量的就是上下文的质量。
也就是说:

下一轮对话到来时,如何在有限的 token 预算里,把真正有价值的上下文重新恢复给模型。

技术上,我了解最粗暴的方式,就是滑动窗口,自动将越界的删除掉。
其次就是摘要压缩,对越界的内容进行压缩后,在携带。但是这样会导致精度问题。
后来就有了层次压缩,如:最近10轮的压缩,10~50轮的轻度压缩,更后方的重度压缩。
但是这会导致一些重要信息,被裁剪忽略掉。
所以后来又出现了重要性过滤。不在一味的按先后顺序。等等…

所以我这里做的是分层恢复策略。
我会将上下文分为好几层进行控制:

第一层放:最新的决策和最近确认过的重要结论,优先级放到最高。
第二层放:当前还没有闭环的问题、待确定的问题。
第三层放:会话级别摘要,用来恢复长对话主线,而不是历史回放。
第四层放:已经沉淀出来的用户偏好、阶段目标、画像信息等。
第五层放:一下RAG召回的经验等。
第六层放:最近的几轮的对话内容。

其中,我把用户最近一次的问题,放到了最后。
因为我之前在读arXiv的一篇论文(读 “archive”),参考他上面的内容:
模型对长上下文的中间信息利用很不稳定,相关信息放到开头或结尾表现会更好。

有兴趣的可以瞅瞅:
在这里插入图片描述
在这里插入图片描述

四、RAG 切分入库

之前已经把协议、提示词、上下文的组成搞定。
但在,我规定的上下文组成的第五层中,是需要RAG召回的,这也就意味着,需要先切分入库

切分入库的质量越高,召回的质量就越高。
最简单的切分方式就是固定大小切分,但是这样很死板,上下文语义会很不完整。
所以就有了语义切分,按照标点符号进行切分。
但有时,先表格、函数代码这些特殊情况,是无法进行语义切分的,所以就需要对这些专项内容切分
其实还有成本更高的父子切分,就是你检索到子片的时候,会把与他相连的上下文一起带回。

而我的本项目,需求不高,所以做的是语义切分

  • 首先按段落切分
  • 如果越界了在按照符号切分。
    并且会保留相邻chunk的150个字符,用来维持语义。

最后在入qdrant与mysql的时。
我是以mysql作为真相数据源存document与元数据。
qdrant 里面存的是向量权限范围用于筛选、和对应mysql的数据索引,用于去数据库中检索。

我在我的代码中也考虑过专项内容切分,考虑到了表格、代码,但是暂时还没有引入语法解析工具这种更高精度的切分。

五、混合召回策略

在上下文压缩的时候,我已经把需要召回的内容,表述的很明白。
但是那个时候为了表述清楚,内部的组成结构,所以我说的还是比较直白死板

通常我会先判断一下两个数组:
最近决策:只有 key_points 不会空才加载。(这个是对话中的要点)
待完成内容:只有 open_loops 不为空才加载,他的存在可以让上下文的连续性更丝滑。
对话压缩后的摘要:长对话必备!
Fact格式化内容:这个达到阈值才能被加载,比如用户性格爱好,最近有啥目标 等用户画像
RAG检索:若检索出来了,需执行度高于阈值,且top前5。
最后就是一些tools信息,与用户最近一次的问题了。

六、我想说的话

本次整体是我自己手写搭建的。
目的就是为了深入理解记忆机制的底层原理
但是如果要在生产级别项目中,为了追求快速开发。
可以复用像 mem0,这种记忆框架。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐