为了账号安全,请及时绑定邮箱和手机立即绑定

让我们来聊聊在线内存中的检索增强生成(RAG)吧!

这是关于Lumos的一系列文章中的第三篇,Lumos 是一个用于辅助浏览网页的 RAG LLM 工具。建议先读前面的文章!。

生成一张关于RAG(检索增强生成)的图片。RAG现在非常流行!大家对此都充满热情!这张图片应模仿《最后的晚餐》的场景。这是一个狂欢派对,每个人对AI、大语言模型以及RAG都兴奋不已!图片应采用有趣的动画风格。让场景充满活力和热闹!

RAG(https://www.promptingguide.ai/techniques/rag)非常火。

如今,我们都对RAG中的“R”部分很熟悉了。“R”代表“RAG”,指的是从向量存储中检索已索引的文档,以补充提示并提供额外上下文,然后将上下文传递给大型语言模型(LLM)。RAG已成为增强LLM的有效框架。RAG管道能够将LLM未训练过的新信息提供给LLM。在微调嵌入模型、搜索优化文档重新排序方面,已经进行了大量的研究和实验,但对于维护复杂的RAG管道所带来的运营负担却很少有人提及。

运营和扩展一个稳健的RAG管道并实现近乎实时的更新需要什么?这是一个巨大的挑战,毫无疑问,。想象一下,将Slack消息实时索引到Elasticsearch集群所需的功夫。这个例子中的数据库操作挑战甚至超过了RAG管道本身的挑战。

随手一发…

在这篇文章中,我想探讨我认为被低估或未充分重视的检索增强生成(RAG)方法:在线生成并存储在内存中的文档嵌入。我们不关注“R”,而是关注在此之前的步骤,也就是检索前的步骤。在Lumos的实现中,文档被索引并存储在LangChain的 [MemoryVectorStore](https://js.langchain.com/docs/integrations/vectorstores/memory) 中,并在索引完成后立即检索。整个流程包括生成嵌入和在检索到文档后提示LLM,并且都是在最终用户发起的单个请求上下文中完成的。

这种方法确实存在一些明显的缺点(例如延迟增加)。然而,我认为这个版本的RAG可能会有更多的未被发现的应用场景,并且在某些情况下可能更加优化(例如更易于操作、更便宜)比一个实时离线架构。本文探讨了在Lumos中实现在线内存中的RAG嵌入生成。

一些建议的定义 📋

在我们开始之前,先弄清楚这两个突出显示的术语会很有帮助。

  1. 在线状态指的是在一个应用程序正常执行过程中发生的过程。相比之下,“离线”指的是在一个应用程序上下文之外发生的过程,不一定是在应用程序运行时发生的。
  2. 内存中指的是应用程序的即时分配的内存。相比之下,外部存储系统(例如 Elasticsearch)则不属于内存中。

简单地浏览一下LangChain的向量存储的相关文档就能发现有很多选择的外部数据存储作为向量存储。

简单的RAG默默地在背后 😅

Lumos 的原始 RAG 管道在每次用户发出提示时都会处理当前标签页上的所有内容。没有任何形式的缓存。每次运行后,内存中初始化的向量存储都会被清空。以下是一个旧的代码片段,展示了原始实现的方式。

    // 把页面内容拆成有重叠的部分  
    const splitter = new RecursiveCharacterTextSplitter({  
      chunkSize: chunkSize,  
      chunkOverlap: chunkOverlap,  
    });  
    const documents = await splitter.createDocuments([context]);  

    // 把文档加载到向量库中  
    const vectorStore = await MemoryVectorStore.fromDocuments(  
      documents,  
      new OllamaEmbeddings({  
        baseUrl: lumosOptions.ollamaHost,  
        model: lumosOptions.ollamaModel,  
      }),  
    );  
    const retriever = vectorStore.asRetriever();  

    // 构建链  
    const chain = RunnableSequence.from([  
      {  
        filtered_context: retriever.pipe(formatDocumentsAsString),  
        question: new RunnablePassthrough(),  
      },  
      formatted_prompt,  
      model,  
      new StringOutputParser(),  
    ]);

这个问题的设计明显有问题。如果用户从同一页面发出多个提示,应用程序会天真地对每个请求重复处理相同的内容。顺序调用Ollama embeddings API并不一定很快。对于包含大量文本的网页,生成所有文档的嵌入可能需要超过一分钟,才能从LLM得到响应。

奥拉玛服务器日志文件

保存向量存储显然是个容易实现的低垂果子,但也需要考虑一些限制因素。

浏览时间来啦!🎉

Lumos 中更新的 RAG 管道受内存限制和用户行为驱动。强调用户行为很重要,因为典型的使用模式很可能是推动其他在线内存 RAG 实现的主要因素。

需要考虑的因素和限制条件 🪨
  1. Chrome的内存有限,不能存储太多的文档在内存中。
  2. 大多数文档在一段时间后会失效。通常情况下,用户不会一直浏览相同的网站,或者网站的内容经常发生变化。
  3. 如果某文档刚刚被索引,就不要再对其进行索引。
  4. 高亮内容是一个例外。高亮内容应该始终在向量存储中索引,以便用于RAG。
  5. 理想情况下,在“浏览过程”结束后应该删除向量存储中的文档。
URL级别的向量存储缓存

鉴于之前的考虑和限制,实现了一个URL级别的向量存储缓存,以便将来自同一URL的文档缓存在一起。由于每个文档本身内在拥有一个唯一的标识符,但这些标识符易于获取且不会产生ID冲突,因此在所有URL中跟踪单个文档具有挑战性。相反,该应用依赖于URL作为来自页面的每个文档的ID。这种方法大大简化了缓存逻辑,并自然符合用户浏览互联网的习惯。在Chrome扩展的持久后台脚本中创建了一个Map对象,用于存储每个URL的MemoryVectorStore对象。

    interface VectorStoreMetadata {  
      vectorStore: MemoryVectorStore  
      createdAt: number  
    }  

    /* 一个从url到向量存储元数据的映射,*/
    const vectorStoreMap = new Map<string, VectorStoreMetadata>();

在每个提示请求的开始,背景脚本会清除所有超过可配置的TTL设置的MemoryVectorStore实例。一个较短的TTL可能看起来有些不寻常,但如果我们考虑我们在互联网上消费的内容类型,我们很少会不断返回到这些包含未改变或很少改变内容的URL。话虽如此,也有一些值得注意的例外(比如说维基百科的文章)。

    // 删除所有过期的向量存储  
    vectorStoreMap.forEach((vectorStoreMetdata: VectorStoreMetadata, url: string) => {  
      if (Date.now() - vectorStoreMetdata.创建时间! > lumosOptions.vectorStoreTTLMins * 60 * 1000) {  
        vectorStoreMap.delete(url);  
        console.log(`正在删除向量存储:${url}`);  
      }  
    });

接下来,声明一个新的向量存储。声明的标识符要么指向已存在于内存中的向量存储(其中所有文档已索引),要么初始化一个新的 MemoryVectorStore,这将触发新嵌入的生成。

    // 检查是否有 URL 的向量存储存在
    var vectorStore: MemoryVectorStore;

    if (!skipCache && vectorStoreMap.has(url)) {
      // 获取 URL 的现有向量存储
      console.log(`获取 URL ${url} 的向量存储`);
      vectorStore = vectorStoreMap.get(url)?.vectorStore!;
    } else {
      // 为 URL ${url} 创建 ${skipCache ? "临时" : "新"} 向量存储
      console.log(`为 URL ${url} 创建 ${skipCache ? "临时" : "新"} 向量存储`);

      // 将页面内容拆分成重叠文档
      const splitter = new RecursiveCharacterTextSplitter({
        chunkSize: chunkSize,
        chunkOverlap: chunkOverlap,
      });
      const documents = await splitter.createDocuments([context]);

      // 将文档加载到向量存储中
      vectorStore = await MemoryVectorStore.fromDocuments(
        documents,
        new OllamaEmbeddings({
          baseUrl: lumosOptions.ollamaHost,
          model: lumosOptions.ollamaModel,
        }),
      );

      // 将向量存储存入映射中
      if (!skipCache) {
        vectorStoreMap.set(url, {
          vectorStore: vectorStore,
          createdAt: Date.now(),
        });
      }
    }

    const retriever = vectorStore.asRetriever();

retriever 在构建 LangChain 链 时会用到。实现还包括了完全跳过缓存流程的功能。这在需要将 高亮内容 传递给后台脚本的场景中是必不可少的。

下一步来啦 📶

在添加缓存之后,后续请求的延迟大大减少。大语言模型的响应几乎是即时的。

奥洛玛服务器日志,连续两个请求的日志

在 Lumos 的 RAG 管道中仍然有很多改进的空间。理想情况下,对 Ollama 嵌入 API 的调用最好是并行进行的。我们可以实施搜索优化技术以及文档重排序。然而,实时离线的方法暂时不予考虑。尤其是对于一个完全客户端应用程序而言,运营外部数据存储的负担超过了在线、内存方法在实现和操作上的简便性。

在线和内存应用的示例 🧠(大脑符号)

在线内存RAG还有哪些其他应用场景?

有几位用户在Hacker News上提到一个用例,他们使用Lumos于聊天产品的网页版应用(比如Discord、WhatsApp),这让我注意到这种用例。

Hacker News 的一张截图

这让我想起了我在 Twitter 上看到的一个有趣的梗,关于将 Slack 消息索引到一个离线的 RAG 管道中(我无法找到这个梗)。_我们真的需要将每个 Slack 消息存入向量数据库吗?我们需要长期保存它们吗?你多久搜索一次一年前的信息?_如果 Slack 成为了公司的主要信息来源,那么这种方法可能有用。如果是这样的话,那么这家公司可能面临更大的问题。

在构建RAG LLM应用时,可以考虑在线内存中的RAG。如果这种方法未能达到预期,总有一旁的Elasticsearch等着 🫣

参考资料
  1. Lumos (GitHub)
  2. 本地浏览器中的大语言模型(第1部分),由Ollama驱动
  3. 本地浏览器中的大语言模型(第2部分),由Ollama驱动
  4. 使用Ollama和LangChain超级充电If语句的提示分类功能(第4部分)
  5. 使用关键词搜索增强内存向量存储(第5部分)
  6. 关于Chrome扩展程序中LangChain文档加载器的注意事项(第6部分)
点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消