当前位置:   article > 正文

LLM - 使用 Langchain 实现本地 Naive RAG_llm langchain 本地

llm langchain 本地

目录

一.引言

二.构建本地 Langchain 库

1.Doc 知识文档

2.Split 文档切分

3.Encode 内容编码

4.Similar 本地库构建

三.缓存本地 Langchain 库

四.读取本地 Langchain 库

1.Load 读取缓存

2.Similar 预测

3.Add 添加文档

五.总结


一.引言

上一篇博客介绍了当下 RAG 的一些发展情况,主要有 Naive RAG、Advanced RAG 以及 Modular RAG,本文通过 Python langchain 库实现一个本地 RAG 的 demo,主要是体会 RAG 搜索增强的流程。本文主要聚焦 Langchain 本地知识库的构建,后续的 LLM 推理因为本机显存的限制,大家可以参考之前推理的博客。

Tips 本文主要从三个方面介绍本地知识库的构建:

- 构建本地 Langchain 知识库

- 缓存本地 Langchain 知识库

- 读取缓存 Langchain 知识库

由于 Langchain 库的更新比较快,有一些 API 的引入方式与用法稍有出入,博主这里的 python 版本为 3.8.6,Langchain 相关 package 版本如下:

二.构建本地 Langchain 库

1.Doc 知识文档

由于是构建本地知识库,所以我们需要获得各个内容的文档,这里我们整理了几篇汽车的新闻作为本地知识库的内容,主要是零跑、理想、小鹏、蔚来汽车的相关新闻。

以 lx.txt 为例,其包含新闻的全部文本:

  1. 有消息透露:截至昨晚(3月4日)MEGA大定还没破万,小定转大定的数量有限,不少客户回流到小鹏X9、腾势D9和问界M9。
  2. 这样的成绩一定是不符合理想原本的预期的。但自MEGA上市以来经历的网络舆论风向,似乎冥冥之中也指向了这个结果……
  3. 经过一年多精心设计的宣传造势,3月1日理想正式推出了它征战纯电市场的首款车型——MEGA。
  4. 大约是为了给新车上市增加一波热搜,在发布会之前,李想(理想汽车创始人、董事长)按照惯例再次亲自下场,试图直接蹭一波“苹果放弃造车”的热度。
  5. 但李厂长万万没想到,这次的剧情没有按照设计好的发展:热搜还是上了,但话题却非常尴尬——MEGA的设计被不少网友直呼像“棺材”。
  6. 汽车的造型设计见仁见智,欣赏不了MEGA的另类造型的确是一些消费者的真实看法。在理想的线下门店,汽车产经记者就遇到多位看车用户直言,对MEGA的配置十分满意,但造型难以接受。
  7. 而在这一波黑MEGA的舆论攻势里,“像棺材”虽说是一种更易于理解的具象化表达,但也确实是非常恶劣了。尤其对国人来说,这个“不吉利”的词一旦和一款车深度绑定,必定会对产品的终端销量造成不小的影响。
  8. 总之,舆论走向超出了最初的预期,理想彻底怒了,官方直接开始举报、删帖+律师函警告。同时,不少站理想的自媒体KOL纷纷下场,斥责这些行为是“下作、无底线的抹黑”。
  9. 其实汽车产品中被送过类似称呼的并非MGEA一个。2015年左右途观事故频发,被消费者调侃为“土棺”。2022年极氪009上市时,也被“赠予”了和MEGA一样的称呼……但这些信息并没有被大肆发酵,尤其是后者,只是新车上市发布时的一个花絮注脚。
  10. 这一次理想一心一意想要打造的“公路高铁”MEGA,为什么被黑地愈演愈烈?
  11. 很多人认为是,舆论反噬。就像网友说的:理想只是遭遇了“回旋镖”。
  12. 今日的车圈营销战不断升级,动辄CEO对线、KOLKOC互踩,李想本人确是“功不可没”的。
  13. 创始人亲自下场蹭热度、宣传话术避重就轻,乃至操纵媒体、内涵友商、攻击自家车主……在理想的发展壮大过程中,这些传播手段被用得炉火纯青。客观讲理想L系列是好产品,但销量也大有营销的“功劳”。
  14. 理想L9上市后,面对“50万的豪车不用铝缺乏诚意”(使用的铁悬架)的质疑,李想选择了人身攻击:“建议觉得铝比钢和铁好的网友们,把自己家的房子钢筋柱结构都拆掉,全换成铝”。
  15. 上周五上市的MEGA被质疑“没有后轮转向”时,理想又在对外宣发中统一口径:后轮转向会让“第三排乘客会感受到更快的横向摆动,会不太舒服”,“后轮转向也不是很厉害的技术,我们的转弯半径也就比L9多一个手掌。”
  16. 类似事例不再赘述,看下网友的精辟总结:
  17. 社交媒体截图社交媒体截图
  18. 对一些有关新车的问题避重就轻,其实不算什么大“黑料”。这原本也是一个车企领袖的基本修养(除了骂人),毕竟人无完人、车无完车。
  19. 而除了怼网友怼友商,理想对媒体的控制和拿捏也达到了“极致”。
  20. 2月份MEGA上市前在三亚组织了一场媒体试驾,所有参与者都签了保密协议。通过这份协议,理想细致拿捏了媒体宣传节奏——仅一次活动安排,让每家媒体按照理想预定的时间和内容方向陆续推送三四次信息,而消费者每天能刷到什么内容,也完全都在掌控之中。
  21. 必须夸一句,当友商们还在思考如何打磨技术,如何做出一场出色的发布会时,理想已经凭借对人性的研究,站在了另一个竞争维度。但那些被操控到如此程度的媒体,恐怕心里总有些不得劲吧。
  22. 不得不提的还有,至今无论对网友还是消费者来说都不可原谅的一件事:去年12月底发生在广东的一起理想L7事故中,有人质疑理想的车辆安全时,李想选择在微博曝光驾驶者的行车记录信息和视频,并且误导舆论指向事故车主超速驾驶!
  23. 这样的理想,被扣上“鸡贼”厂的名号由来已久,甚至理想自家车主对理想和李想的态度是:车是好车,人不认同。
  24. 此次MEGA宣发翻车,一方面因为理想流量太盛,枪打出头鸟,不排除真是竞争对手有意为之玩了一手“以彼之道还施彼身”;但也一定煽动了早就看不惯理想做事风格的网友和媒体,跟风行动。
  25. 不管是谁策动,都不由得让人想把李厂长diss友商的经典名句返送给他自己:“这点作战都受不了,难道是巨婴?”
  26. 而为了MEGA,这个回旋镖,理想是必须要承受的。
  27. 周末,北京最大新势力门店聚集地“蓝色港湾”,理想的门店在中午时段甚至需要排队。大家多为MEGA而来,有的感兴趣,有的出于好奇。
  28. 不可否认MEGA是一款足够吸引眼球的产品,但这样的关注度有多少转化为最终销量?还很难说。从增程转战纯电的理想,要重新接受考验。
  29. 很多人也都注意到,这次MEGA上市后李想并没有像往常一样回怼各种质疑,也没有迫不及待分享传说中的订单数据。甚至在发布会后理想还经历了股价的下滑。
  30. MEGA的舆论和销量会如何相互裹挟着前行?这一波难堪的舆论风波后,李想又会不会对此有所反思?静观其变。

2.Split 文档切分

对于通用文本,这里建议使用 RecursiveCharacterTextSplitter 分割器进行文本切分。

  1. from langchain_community.document_loaders import TextLoader
  2. from langchain_community.embeddings import HuggingFaceEmbeddings
  3. from langchain_community.vectorstores.chroma import Chroma
  4. from langchain_text_splitters import RecursiveCharacterTextSplitter
  5. # 读取原始文档
  6. raw_documents_lp = TextLoader('/Users/xxx/langchain/LocalDB/lp.txt', encoding='utf-8').load()
  7. raw_documents_lx = TextLoader('/Users/xxx/langchain/LocalDB/lx.txt', encoding='utf-8').load()
  8. # 分割文档
  9. text_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=0)
  10. documents_lp = text_splitter.split_documents(raw_documents_lp)
  11. documents_lx = text_splitter.split_documents(raw_documents_lx)
  12. documents = documents_lp + documents_lx
  13. print("documents nums:", documents.__len__())

这里加载零跑与理想的文档,如果文档多的同学直接 os.path 遍历 for 循环添加即可,我们最终得到的是多个通过 text_splitter 分割的 document 文档。其主要包含 page_content 和 metadata 两个属性,前者包含分割后的文本块 Chunk,后者包含一些元信息,主要是文档内容来源。

运行上述代码后会得到分割后文档数量,其中 chunk_size 代表每个块的保留大小,chunk_overlap 代表前后 content 是否有重叠,类似滑动窗口一样。

documents nums: 75

3.Encode 内容编码

由于需要通过向量存储与检索 Top-K,所以需要对应的编码器生成对应 content 的 Embedding,这里我们选择通过 HuggingFaceEmbeddings 方法来生成文本的 Embedding。

  1. # 生成向量(embedding)
  2. embedding_model_dict = {
  3. "mini-lm": "/Users/xxx/model/All-MiniLM-L6-V2"
  4. }
  5. EMBEDDING_MODEL = "mini-lm"
  6. embeddings = HuggingFaceEmbeddings(model_name=embedding_model_dict[EMBEDDING_MODEL])

由于网络连接的问题,这里博主建议把模型下载到本地文件夹中直接加载,登录 HuggignFace 官网 https://huggingface.co/sentence-transformers/ 可以检索到多个文本编码的模型:

这里我们选择轻量级的 all-MiniLM-L6-v2 作为 Embedding 编码的模型,手动一个一个下载或者挂着镜像用 API 下载都可以:

执行完毕后我们获得一个可以编码的 Embedding 模型: 

4.Similar 本地库构建

  1. db = Chroma.from_documents(documents, embedding=embeddings)
  2. # 检索
  3. query = "理想汽车怎么样?"
  4. docs = db.similarity_search(query, k=5)
  5. # 打印结果
  6. for doc in docs:
  7. print("===")
  8. print("metadata:", doc.metadata)
  9. print("page_content:", doc.page_content)

通过本地分割好的文档 documents 与指定的 embedding 模型我们构建本地 Langchain DB,通过 query 与 sim_search API 进行 Top-k 文本的获取,得到的 doc 我们可以获取其 metadata 即来源以及其对应的文本:

可以看到 5 条中有 4 条来自 lx.txt 即理想的文档,而一条来自 lp.txt 即零跑汽车,基于这些 page_content,我们还需要做清洗、合并等处理才能得到最终的增强信息,对用户的原始 Query 进行扩展得到最终的 Prompt 再输入 LLM 得到回复。

三.缓存本地 Langchain 库

如果不想每次都处理加载文档再构建 DB 可以预先处理并把 DB 做本地的 cache,用的时候直接读取 cache 加载即可。

  1. def persist():
  2. raw_documents_news = TextLoader('/Users/xxx/langchain/lx.txt', encoding='utf-8').load()
  3. text_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=0)
  4. documents_news = text_splitter.split_documents(raw_documents_news)
  5. embedding_model_dict = {
  6. "mini-lm": "/Users/xxx/model/All-MiniLM-L6-V2"
  7. }
  8. EMBEDDING_MODEL = "mini-lm"
  9. # 初始化 huggingFace 的 embeddings 对象
  10. embeddings = HuggingFaceEmbeddings(model_name=embedding_model_dict[EMBEDDING_MODEL])
  11. db = Chroma.from_documents(documents_news, embeddings, persist_directory="./local_cache")
  12. db.persist()
  13. print("Save Success ...")

执行后在 cache 对应文件下生成如下文件即为成功: 

缓存大小为 2mb:

四.读取本地 Langchain 库

1.Load 读取缓存

  1. embedding_model_dict = {
  2. "mini-lm": "/Users/xxx/model/All-MiniLM-L6-V2"
  3. }
  4. EMBEDDING_MODEL = "mini-lm"
  5. # 初始化 huggingFace 的 embeddings 对象
  6. embeddings = HuggingFaceEmbeddings(model_name=embedding_model_dict[EMBEDDING_MODEL])
  7. db = Chroma(persist_directory="/Users/xxx/langchain/local_cache", embedding_function=embeddings)

同样需要加载 embedding 模型,但是 doc 内容直接从 cache 中获取,通过 persist_directory 方法获取 Chroma Database。

2.Similar 预测

  1. # 检索
  2. query = "理想汽车"
  3. docs = db.similarity_search(query, k=5)
  4. # 打印结果
  5. for doc in docs:
  6. print("===")
  7. print("metadata:", doc.metadata)
  8. print("page_content:", doc.page_content)
  9. exit(0)

3.Add 添加文档

本地库存在更新慢的情况,读取缓存后如果有新的 doc 可以调用 db.add 方法添加,随后再执行查询,下面我们在 cache 的基础上引入小鹏汽车 xp.txt 的信息,并预测新的 query。

  1. # 添加文档
  2. text_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=0)
  3. raw_documents_xp = TextLoader('/Users/xxx/langchain/LocalDB/xp.txt', encoding='utf-8').load()
  4. documents_news = text_splitter.split_documents(raw_documents_xp)
  5. db.add_documents(documents_news)

加载本地 Langchain 库后,我们可以继续将新增的本地 doc 添加至 DB 中,下面我们再测试下,这次寻找与新增小鹏汽车相关的信息:

  1. # 检索
  2. query = "小鹏汽车"
  3. docs = db.similarity_search(query, k=5)
  4. # 打印结果
  5. for doc in docs:
  6. print("===")
  7. print("metadata:", doc.metadata)
  8. print("page_content:", doc.page_content)
  9. exit(0)

xp.txt 里小鹏汽车的关键字比较多,所以匹配下来 metadata 都指向 xp.txt,不存在之前 lx 检索到 lp 的情况: 

五.总结

上面简单测试了基于 Doc 构建本地 Langchain 库的一些方法,关于更细粒度的 Langchain 和 RAG,还涉及到很多细节的点,包括对 query 的清洗与处理,对文档的清理与筛选,对 Langchain 结果的取舍与合并以及 LLM Prompt 的构建,这些细致的点大家可以一一扩散提高搜索的效果。

Tips:

Langchain 中文 API 介绍: https://www.wpsshop.cn/w/不正经/article/detail/221551

推荐阅读
相关标签
  

闽ICP备14008679号