这个也完成了,不愧是我。
这个完成的是在只拥有api,无法训练AI的情况下,如何通过语义检索搭配关键词解锁AI长期记忆。
我最最最开始是想用AI提取关键词然后写个程序匹配文本搜索相关的历史条目,但是这样就有两个问题,一是效率不高,耗时长。向AI发送请求需要时间,能处理的文本也少,毕竟通用大模型本身不是用来执行这种事的;二是之前我提过的,关键词在不同句子中可能语义不同,就会匹配到错误的会话里。
不过根据关键词数量调整权重这个想法我自认为还算不错,这是比语义要好的地方,语义真的是会莫名引导到不相干的地方,并且相似度还莫名的高,这是语义的算法导致的。
接下来逐步讲讲思路和流程。
干扰词筛选
这是多次试验的结果,也是解决非关键词影响语义的措施。
比如口头称呼在对话中占比非常多,换用不同的口头称呼就导致语义不同,不能找到最恰当的结果,因此需要对输入文本和历史消息都进行干扰词筛选。
方法就是储存为变量时直接删去干扰词就好了,干扰词可以放在一个数组里。
向量化压缩文本
是指将文本转换成向量形式,并且在这个过程中进行压缩。
直接用文本处理效率不高,占用空间大,而向量化之后可以更紧凑,同时保留关键信息。并且向量化还有个优势就是可以具象化地算出两个文本之间的相似度。
向量化压缩的方法:
//加载预训练模型,初始化模型实例
model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
//由文本内容生成查询向量
vectors = model.encode(contents, show_progress_bar=True)
这里使用的方法是 SentenceTransformer,它是一个开源的预训练模型,可以将文本转换成向量形式,这些向量随后会被用于构建 FAISS 索引以进行相似度检索
语义检索
基于语义而非关键词匹配的检索技术,通过向量相似性找到相关内容。
我在尝试多次后发现给关键词加权重反而会破坏语义检索,导致结果不如单独的语义检索。
索引构建
vector_dim = vectors.shape[1]#从向量集合中获取向量维度
index = faiss.IndexHNSWFlat(vector_dim, 32) # 创建HNSW索引
index.hnsw.efConstruction = 40 # 设置构建参数,efConstruction越大越有可能找到真正的邻居,但索引大小也越大,会增加构建时间和内存开销
index.hnsw.efSearch = 64 # 设置搜索参数,访问 FAISS 索引的 HNSW (Hierarchical Navigable Small World) 结构。HNSW 是一种用于高效近似最近邻搜索的数据结构。此值决定了搜索过程中访问的最大节点数,越大则搜索越精确,但也越慢。
index.add(vectors) # 向索引中添加向量
相似度计算
k = 10 # 设定返回结果数
D, I = index.search(query_vector, k) # 搜索索引,返回距离和索引
# D是一个数组,包含查询向量与检索结果向量之间的距离(默认情况下,这个距离是余弦相似度的倒数,距离越小,相似度越高)
# I 是一个数组,包含检索结果的索引,这些索引对应于原始文本内容列表中的位置。
results = []
for i in range(k):
results.append(contents[I[0][i]]) # 获取搜索结果
计算所得的相似度在-1到1之间,数值越接近1,表示两个文本越相似。反之则语义相反,由于这里使用的 FAISS 索引,所以一般不会有小于0的。
另外,相似度依旧可以通过乘以特定数值来给不同文本分配比重。比如用户与AI的对话中,用户的比重可以比AI高一些。
关键词匹配
keywords = ['AI', '长期记忆'] # 设定关键词
for keyword in keywords:
keyword_vector = model.encode(keyword)[0] # 计算关键词的向量
query_vector += keyword_vector # 向查询向量中添加关键词的向量
query_vector /= len(keywords) # 计算平均向量
虽然最后舍弃了关键词匹配,但还是记一下。
不过在语义检索得到结果之后,依旧可以在少量结果的范围内进行关键词筛选。
排序筛选
results.sort(key=lambda x: D[0][results.index(x)]) # 按相似度排序
results = [result for result in results if all(keyword in result for keyword in keywords)] # 关键词筛选结果
最后可以取前几个结果,并添加相似度限制,比如必须高于0.5,没有找到就返回空,避免无关回答干扰结果。
最后当然是效果展示环节:
简单的语义检索测试脚本(python)地址:
https://github.com/Sanqi-normal/tools/blob/main/%E8%AF%AD%E4%B9%89%E6%A3%80%E7%B4%A2/long_memory.py
如果你奇怪python脚本如何能够被node环境利用,实际上我是让python脚本占用了端口开放检索记忆的服务,然后后端发送http请求。
当然这样不适用后续生产环境,临时搭建测试而已。
后续补充:
不足:
- 精度不高,文本信息量不单一时出现不准确的筛选情况时有发生,即使关键词也没办法有效纠正。如果无法做到对历史记录的准确检索,那长期记忆就无法有效发挥作用。
- 或许应该寻求更精确的语义模型/关键词检索模型
- 历史缺失。由于我的程序每隔几次会话都主动进行过去短期历史记录的压缩以减少输入,这种情况还好;但多次对话下必然无法承受历史记录的积累,需要把最早的卸下来或者降低历史摘要的精度。而这部分由于语义检索是静态的而不能被添加到检索中,因而就缺失了。
- 冗长的一轮对话后重新启动服务器,或者趁用户等待返回信息的时候悄悄重启,无缝填充。当然,这是用户体验的部分。
- 关键词筛选。“上大学”会返回多条记录,“上学”却不能,这是关键词筛选的问题,把符合语义的筛选掉了。但不用关键词筛选,有时候返回的结果太挨不着边了。
仍需优化。
此方悬停