终于终于,我们要聊一聊一点比较有意思的内容了。这一次,我们将会简单地介绍 RAG 及相关技术和概念。并分享一下我对 RAG 技术与“机器记忆”之间关系的理解。根据我们讨论的内容,我们将会简单地实践一下由 AI 控制的标签 RAG,用于永久记忆的简单辅助检索。

前言

在本系列文章中,LLM 是我们经常提到的概念,它在最近发展地很迅猛,比如最近的 deepseek R1 系列,凭借较低的推理成本和较好的质量且开源,在 LLM 的历史上留下了不凡的一笔。反观某 CloseAI,高下立判,OpenAI 啊,你可长点心吧……但是,话又说回来,即便现代 LLM 如此强大,其仍然有一个我认为比较致命的缺点:模型是固定的,很难实时更新数据和参数。这意味着模型本身是静态的。我们当然可以通过诸如 “Finetune”, “LORA”, “RLHF” 等方法来改变 LLM 的输出倾向,但这仍然不是从根本上更新知识的方法。除非……几乎完整地预训练(效果最好),或者“持续学习”,“部分预训练”(效果一般)等技术。更别提复杂的数据清洗和分选、提炼的步骤。可以说,就目前而言,想要快速迭代一款相对可用的大型 LLM ,无论是对于个人还是一般企业,这都是一件不太现实的事情。归根到底,这是 LLM 这类模型本质上设计之初就不是为了实时迭代信息考虑的。当然我猜测可能是因为引入时间序列会导致预测函数变成一坨极为复杂的非线性函数罢。~~(绝对不是我偷听 Richard S. Sutton 老爷子的观点)~~总之,就目前而言,低成本的在线学习 LLM 技术可能还不是很成熟,如果我们想要相对简单地让 LLM 拥有实时知识,就我粗浅的认识里大概就是:Prompt 里引入一个分区,通过外挂数据库或者实时获取信息并插入 Prompt 的某个分区来实现相对准确的知识更新罢。

这个想法听起来很简单粗暴,但实际工程上实施起来还有很多挑战。比如如何从性能、成本、实时性三者中取得平衡达到一个相对最优的方案?如何根据不同的场景动态调整检索/写入策略?对于记忆特化方面,我们还能做出什么优化?等等问题。而我们今天就将浅谈这些问题。

一些概念

词嵌入

在自然语言处理中,词嵌入是对一个词的表示。该嵌入用于文本分析。通常,这种表示是一个实值向量,它以某种方式编码词的含义,使得在向量空间中更接近的词在意义上也被期望是相似的。词嵌入可以通过语言建模和特征学习技术获得,其中词汇表中的词或短语被映射到向量的实数。

很难理解对不对?因为上面的摘录自维基百科😅,我用通俗的语言解释一下:

词嵌入是让计算机“理解”词语含义的技术。它的核心思想是将每个词转换为一个高高高高维度的数字向量(比如 [0.3, -1.2, 4.5]),这个向量可以看作词语在“语义空间”中的坐标。例如,“苹果”和“橘子”的向量距离较近(因为它们都是水果),而“苹果”和“汽车”的向量距离较远。这种表示方式让计算机能通过向量计算捕捉词语之间的语义关系。

词嵌入有什么用呢

传统 AI 处理文本时,只能看到孤立的字符或词频(例如:数数这篇文章里有几个“记忆”一词?),但无法理解词与词之间的关联。词嵌入通过以下两点解决了这一问题:

  1. 语义相似性:将语义相近的词映射到向量空间中相邻的位置。例如“医院”和“诊所”的向量接近。
  2. 语义关系建模:支持类比推理,比如“国王 - 男 + 女 ≈ 王后”。这种能力对复杂任务(如记忆系统)至关重要。 “原神 - 福利 + 剧情不能跳过 + 出货下毒 ≈ 依托答辩”
  3. **语义关系分类:**我们可以根据几个个词语(在高维空间中就是几个坐标点)进行以此词组进行分类或者筛选,比如二维向量里,两个坐标连成一条线,线上面的部分和下面的部分里的坐标点就可以分成两类,这是基于我们选定的词语的概念进行分类的。这在提高检索速度和精确度/语义理解方面很有帮助。(严格地说不是这种分类方法,但为了便于理解)

在RAG里的行为

  1. 问题向量化:当用户提问“如何缓解感冒症状?”时, RAG 会先将问题转换为简单的问题词组,进而将词组转为嵌入向量。
  2. 语义检索:在文档库中搜索与问题向量最接近的文本片段(例如“感冒的家庭护理建议:多休息、补充水分……”),而非依赖简单的关键词匹配。
  3. 增强生成:生成模型(如GPT)基于检索到的内容输出回答,确保信息准确且相关。

一些其他的补充……

最开始的嵌入模型如Word2Vec、GloVe是静态的(每个词只有一个向量),而现代模型能生成上下文相关的动态向量(例如“苹果”在“吃苹果”和“苹果手机”中的向量就不同);现在RAG系统通常直接使用句子或段落嵌入(比如OpenAI的text-embedding-3-small),但底层原理与词嵌入一脉相承。 嵌入实际上是一种量化与编码技术,不仅仅局限于文字,我们当然也可以提取图片,视频,音频的特征并进行嵌入,这就是混合 RAG 相关的内容了。还有,我们可以把不同信息之间的关系也嵌入,就想套娃一样,层层关系层层精炼,这又是 Graph RAG 相关的内容了。总之,玩法很多,但有一点,机器处理信息是有极限的,无论任何系统,在没有自纠错措施的前提下,其错误率只能无限接近于人类提供的训练集的错误率,这个会影响记忆系统的设计理念

RAG里都有啥?

RAG Diagram

(By Turtlecrown - Own work, CC BY-SA 4.0)

注意,这是比较典型的 RAG 系统图,实际使用的时候是非常灵活的,但大多包含三个模块:检索器(Retriever),生成器(Generator),写入器(Injector)。

检索器(Retriever)

检索器负责从外部数据库(比如向量数据库与对应文件簇等)中快速检索与输入问题相关的文件/文本片段。一般会有稀疏检索或者稠密检索啥的。前者是传统的匹配算法,后者就与我们刚刚讲到的嵌入有关系了,因为后者与语义相关。无论如何,检索器需要平衡召回率和检索速度,虽然后续生成器理论上比检索器慢,但是能优化的我们还是要尽量优化的。

我们先简单理解检索器:检索器负责找到所有粗略的,与问题相关的内容,但是其中可能会包含很多不相关的甚至是反向影响答案的内容,但是检索器不管,它一股脑的都检索到位,根据 RAG 的理念,多余的信息也比缺失信息好。后续生成器会分析检索器返回的内容并整理。即便如此,检索器的优化空间仍十分巨大,对于不同类型的任务动态选择检索模式会有很大的提升。比如比较主流的混合检索方案就使用并发 BM25 + 模糊搜索 + DPR 的形式混合检索,但是这个方案当时好像在几个开源 RAG 项目里是收费的,现在也不知道免费了没有……

生成器(Generator)

生成器负责综合分析检索到的上下文,输入问题,问题的上下文,生成最终答案。简单地理解为:整合所有信息并决策哪些信息是真正能够回答问题的,选择性地保留信息,尝试给出最佳答案。

这里也有优化空间,我们既可以使用多个不同 Prompt 的 LLM 并发生成并应用注意力机制,也可以使用更个性化的权重控制,比如使用遗忘曲线/标签/环境感知等方法来得到更精准的回复。

其他辅助/优化组件

弄个非关系型数据库进行辅助索引,提高性能降低向量库开销;还有 Agent RAG,相对更智能(烧钱)一些的 RAG,用 LLM 辅助决策 RAG 策略;引入联网搜索/根据现实传感器数据实时决策的 RAG 等等等等……

水多加面,面多加水

向量数据库

是专为处理高维向量数据而设计的数据库系统,它像一座智能图书馆,不仅能存储海量的非结构化数据(如文本、图片、音频),还能通过“语义理解”快速找到最相关的内容。与传统数据库依赖关键词匹配不同,向量数据库的核心在于将数据转化为向量(一组数字组成的“指纹”),并通过计算向量之间的距离(如余弦相似度)衡量它们的关联性。这样就和我们刚刚讲到的嵌入技术连接起来了!嵌入生成的向量就可以存到向量数据库里面,以便后续的索引/查询/更新等。

Milvus

Milvus 是一个开源的向量数据库,很强大,性能很强,文档写的也好。它通过分布式架构和高效的索引算法(比如 HNSW、IVF),实现了对亿级数据的毫秒级检索(可以用不上,但是不能没有!)。这种能力让向量数据库成为构建 RAG 系统的核心支柱,尤其在需要实时响应和动态更新记忆的场景里,它既是“记忆中枢”,也是“信息过滤器”。Milvus 的索引和检索功能很强大,完全可以作为检索器来使用,搭配其他检索器也很好用!

当然也有一些本地的轻量级的向量数据库或者免费的在线服务,比如 Pinecone 或者 Chroma 啥的,这里就不过多赘述了,原理上相差不大,请读者自行参考对应文档学习如何使用。本教程以 Milvus 向量数据库为例。

向量数据库里有几个概念我们需要了解一下:

  • 集合(Collection):存储向量和元数据的容器(类似数据库的表),虽然有动态 Schema,但是如果不加以分类,也是会乱成一锅粥。
  • 索引(Index):加速向量检索的数据结构(如IVF_FLAT、HNSW等),当我们存进去一堆向量后,数据库会预生成一个索引,检索的时候使用这个数据结构索引,大大提高速度。
  • 分区(Partition):逻辑分片,优化大规模数据管理,如果你的记忆是好几千年万年的那种,可能会用上……

实战一下!

写了这么多,我自己都快要写睡着了……不如直接来点干货,我们先搭建一个非常非常简单的 RAG 用于检索我们上一篇文章的会话总结记忆吧!

部署 Milvus 向量数据库

老方法,Docker Compose 部署,管理方便,备份/还原也方便。首先创建一个文件夹用于存放 Milvus 相关的 Compose 文件:

mkdir milvus
cd milvus

然后创建 docker-compose.yaml 文件:

vim docker-compose.yaml

配置文件:

version: '3.5'

services:
  etcd:
    container_name: milvus-etcd
    image: quay.io/coreos/etcd:v3.5.5
    environment:
      - ETCD_AUTO_COMPACTION_MODE=revision
      - ETCD_AUTO_COMPACTION_RETENTION=1000
      - ETCD_QUOTA_BACKEND_BYTES=4294967296
      - ETCD_SNAPSHOT_COUNT=50000
    volumes:
      - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd
    command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd
    healthcheck:
      test: ["CMD", "etcdctl", "endpoint", "health"]
      interval: 30s
      timeout: 20s
      retries: 3

  minio:
    container_name: milvus-minio
    image: minio/minio:RELEASE.2023-03-20T20-16-18Z
    environment:
      MINIO_ACCESS_KEY: minioadmin
      MINIO_SECRET_KEY: minioadmin
    ports:
      - "9001:9001"
      - "9000:9000"
    volumes:
      - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data
    command: minio server /minio_data --console-address ":9001"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 30s
      timeout: 20s
      retries: 3
      
  standalone:
    container_name: milvus-standalone
    image: milvusdb/milvus:v2.4.6
    command: ["milvus", "run", "standalone"]
    security_opt:
    - seccomp:unconfined
    environment:
      ETCD_ENDPOINTS: etcd:2379
      MINIO_ADDRESS: minio:9000
    volumes:
      - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus
      - ./milvus.yaml:/milvus/configs/milvus.yaml
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"]
      interval: 30s
      start_period: 90s
      timeout: 20s
      retries: 3
    ports:
      - "19530:19530"
      - "9091:9091"
    depends_on:
      - "etcd"
      - "minio"

networks:
  default:
    name: milvus
      driver: bridge
                      

(是的,看起来很复杂……)

完全独立的 compose 文件有助于单独维护,例如数据库自动备份/恢复,集群等。

然后照旧保存退出。

配置 Milvus

Milvus 的配置需要 milvus.yaml 文件,Milvus 官方提供了一个配置文件的模版,我们可以使用这个链接/命令下载:

wget https://raw.githubusercontent.com/milvus-io/milvus/v2.5.4/configs/milvus.yaml

请注意根据你的版本和镜像,以及部署方式选择版本号和标签。

你可以选择添加验证登陆以及更多配置项目,本教程不过多赘述,请参考官方文档:官方文档

在本教程里,我们使用全默认配置,后续你可以增加反向代理,设置验证方式以及自动备份等。

我们尝试第一次启动:

docker-compose up -d

第一次可以不加 -d 检测一下有没有问题,但是实际上日志跳的太快了也不方便……如果没有任何问题,我们将使用Milvus Python SDK远程访问向量数据库。

通过 Python SDK 进行简单的数据库操作

初始化与新建

首先我们安装 SDK

pip install -U pymilvus

然后我们创建一个数据库,就命名为 cyberai_mem 吧。

from pymilvus import connections, db
conn = connections.connect(host="127.0.0.1", port=19530) # 替换成你实际的 Milvus server 的地址和端口
database = db.create_database("cyberai_mem")

你也可以这样写:

conn = connections.connect(
    host="127.0.0.1",
    port="19530",
    db_name="cyberai_mem"
)

当然,如果创建数据库或者创建集合等等等等,使用脚本或命令太麻烦,这里有一个带有 GUI 的客户端我们可以使用:Attu

使用这个 GUI 可以进行一些简单的数据库操作。

向量搜索,小子

连接到数据库

连接到数据库!

from pymilvus import MilvusClient
MilvusClient = MilvusClient(
    uri="http://127.0.0.1:19530",  # 替换成你实际的 Milvus server 的地址和端口
    db_name="cyberai_mem",
)

创建集合

然后我们创建集合!在这里指定集合的名字和里面向量的维度:

if client.has_collection(collection_name="demo_collection"):
    client.drop_collection(collection_name="demo_collection")
client.create_collection(
    collection_name="demo_collection",
    dimension=1536,  # The vectors we will use in this demo has 1536 dimensions, 与你的嵌入模型输出维度数量相同
)

注意,这里使用的其实是默认的集合配置,包括向量索引方法和模板都是默认,如果有机会后续高级教程再介绍动态 Schema 和自定义模版吧……(挖坑)

在默认设置里,主键和向量字段使用它们的默认名称(“id”和“vector”),度量类型(向量距离定义)已设置为默认值(COSINE)。

准备数据然后向量化

未完待续