目录
一.引言
二.构建本地 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 为例,其包含新闻的全部文本:
有消息透露:截至昨晚(3月4日)MEGA大定还没破万,小定转大定的数量有限,不少客户回流到小鹏X9、腾势D9和问界M9。
这样的成绩一定是不符合理想原本的预期的。但自MEGA上市以来经历的网络舆论风向,似乎冥冥之中也指向了这个结果……
经过一年多精心设计的宣传造势,3月1日理想正式推出了它征战纯电市场的首款车型——MEGA。
大约是为了给新车上市增加一波热搜,在发布会之前,李想(理想汽车创始人、董事长)按照惯例再次亲自下场,试图直接蹭一波“苹果放弃造车”的热度。
但李厂长万万没想到,这次的剧情没有按照设计好的发展:热搜还是上了,但话题却非常尴尬——MEGA的设计被不少网友直呼像“棺材”。
汽车的造型设计见仁见智,欣赏不了MEGA的另类造型的确是一些消费者的真实看法。在理想的线下门店,汽车产经记者就遇到多位看车用户直言,对MEGA的配置十分满意,但造型难以接受。
而在这一波黑MEGA的舆论攻势里,“像棺材”虽说是一种更易于理解的具象化表达,但也确实是非常恶劣了。尤其对国人来说,这个“不吉利”的词一旦和一款车深度绑定,必定会对产品的终端销量造成不小的影响。
总之,舆论走向超出了最初的预期,理想彻底怒了,官方直接开始举报、删帖+律师函警告。同时,不少站理想的自媒体KOL纷纷下场,斥责这些行为是“下作、无底线的抹黑”。
其实汽车产品中被送过类似称呼的并非MGEA一个。2015年左右途观事故频发,被消费者调侃为“土棺”。2022年极氪009上市时,也被“赠予”了和MEGA一样的称呼……但这些信息并没有被大肆发酵,尤其是后者,只是新车上市发布时的一个花絮注脚。
这一次理想一心一意想要打造的“公路高铁”MEGA,为什么被黑地愈演愈烈?
很多人认为是,舆论反噬。就像网友说的:理想只是遭遇了“回旋镖”。
今日的车圈营销战不断升级,动辄CEO对线、KOLKOC互踩,李想本人确是“功不可没”的。
创始人亲自下场蹭热度、宣传话术避重就轻,乃至操纵媒体、内涵友商、攻击自家车主……在理想的发展壮大过程中,这些传播手段被用得炉火纯青。客观讲理想L系列是好产品,但销量也大有营销的“功劳”。
理想L9上市后,面对“50万的豪车不用铝缺乏诚意”(使用的铁悬架)的质疑,李想选择了人身攻击:“建议觉得铝比钢和铁好的网友们,把自己家的房子钢筋柱结构都拆掉,全换成铝”。
上周五上市的MEGA被质疑“没有后轮转向”时,理想又在对外宣发中统一口径:后轮转向会让“第三排乘客会感受到更快的横向摆动,会不太舒服”,“后轮转向也不是很厉害的技术,我们的转弯半径也就比L9多一个手掌。”
类似事例不再赘述,看下网友的精辟总结:
社交媒体截图社交媒体截图
对一些有关新车的问题避重就轻,其实不算什么大“黑料”。这原本也是一个车企领袖的基本修养(除了骂人),毕竟人无完人、车无完车。
而除了怼网友怼友商,理想对媒体的控制和拿捏也达到了“极致”。
2月份MEGA上市前在三亚组织了一场媒体试驾,所有参与者都签了保密协议。通过这份协议,理想细致拿捏了媒体宣传节奏——仅一次活动安排,让每家媒体按照理想预定的时间和内容方向陆续推送三四次信息,而消费者每天能刷到什么内容,也完全都在掌控之中。
必须夸一句,当友商们还在思考如何打磨技术,如何做出一场出色的发布会时,理想已经凭借对人性的研究,站在了另一个竞争维度。但那些被操控到如此程度的媒体,恐怕心里总有些不得劲吧。
不得不提的还有,至今无论对网友还是消费者来说都不可原谅的一件事:去年12月底发生在广东的一起理想L7事故中,有人质疑理想的车辆安全时,李想选择在微博曝光驾驶者的行车记录信息和视频,并且误导舆论指向事故车主超速驾驶!
这样的理想,被扣上“鸡贼”厂的名号由来已久,甚至理想自家车主对理想和李想的态度是:车是好车,人不认同。
此次MEGA宣发翻车,一方面因为理想流量太盛,枪打出头鸟,不排除真是竞争对手有意为之玩了一手“以彼之道还施彼身”;但也一定煽动了早就看不惯理想做事风格的网友和媒体,跟风行动。
不管是谁策动,都不由得让人想把李厂长diss友商的经典名句返送给他自己:“这点作战都受不了,难道是巨婴?”
而为了MEGA,这个回旋镖,理想是必须要承受的。
周末,北京最大新势力门店聚集地“蓝色港湾”,理想的门店在中午时段甚至需要排队。大家多为MEGA而来,有的感兴趣,有的出于好奇。
不可否认MEGA是一款足够吸引眼球的产品,但这样的关注度有多少转化为最终销量?还很难说。从增程转战纯电的理想,要重新接受考验。
很多人也都注意到,这次MEGA上市后李想并没有像往常一样回怼各种质疑,也没有迫不及待分享传说中的订单数据。甚至在发布会后理想还经历了股价的下滑。
MEGA的舆论和销量会如何相互裹挟着前行?这一波难堪的舆论风波后,李想又会不会对此有所反思?静观其变。
2.Split 文档切分
对于通用文本,这里建议使用 RecursiveCharacterTextSplitter 分割器进行文本切分。
from langchain_community.document_loaders import TextLoader
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores.chroma import Chroma
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 读取原始文档
raw_documents_lp = TextLoader('/Users/xxx/langchain/LocalDB/lp.txt', encoding='utf-8').load()
raw_documents_lx = TextLoader('/Users/xxx/langchain/LocalDB/lx.txt', encoding='utf-8').load()
# 分割文档
text_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=0)
documents_lp = text_splitter.split_documents(raw_documents_lp)
documents_lx = text_splitter.split_documents(raw_documents_lx)
documents = documents_lp + documents_lx
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。
# 生成向量(embedding)
embedding_model_dict = {
"mini-lm": "/Users/xxx/model/All-MiniLM-L6-V2"
}
EMBEDDING_MODEL = "mini-lm"
embeddings = HuggingFaceEmbeddings(model_name=embedding_model_dict[EMBEDDING_MODEL])
由于网络连接的问题,这里博主建议把模型下载到本地文件夹中直接加载,登录 HuggignFace 官网 https://huggingface.co/sentence-transformers/ 可以检索到多个文本编码的模型:
这里我们选择轻量级的 all-MiniLM-L6-v2 作为 Embedding 编码的模型,手动一个一个下载或者挂着镜像用 API 下载都可以:
执行完毕后我们获得一个可以编码的 Embedding 模型:
4.Similar 本地库构建
db = Chroma.from_documents(documents, embedding=embeddings)
# 检索
query = "理想汽车怎么样?"
docs = db.similarity_search(query, k=5)
# 打印结果
for doc in docs:
print("===")
print("metadata:", doc.metadata)
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 加载即可。
def persist():
raw_documents_news = TextLoader('/Users/xxx/langchain/lx.txt', encoding='utf-8').load()
text_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=0)
documents_news = text_splitter.split_documents(raw_documents_news)
embedding_model_dict = {
"mini-lm": "/Users/xxx/model/All-MiniLM-L6-V2"
}
EMBEDDING_MODEL = "mini-lm"
# 初始化 huggingFace 的 embeddings 对象
embeddings = HuggingFaceEmbeddings(model_name=embedding_model_dict[EMBEDDING_MODEL])
db = Chroma.from_documents(documents_news, embeddings, persist_directory="./local_cache")
db.persist()
print("Save Success ...")
执行后在 cache 对应文件下生成如下文件即为成功:
缓存大小为 2mb:
四.读取本地 Langchain 库
1.Load 读取缓存
embedding_model_dict = {
"mini-lm": "/Users/xxx/model/All-MiniLM-L6-V2"
}
EMBEDDING_MODEL = "mini-lm"
# 初始化 huggingFace 的 embeddings 对象
embeddings = HuggingFaceEmbeddings(model_name=embedding_model_dict[EMBEDDING_MODEL])
db = Chroma(persist_directory="/Users/xxx/langchain/local_cache", embedding_function=embeddings)
同样需要加载 embedding 模型,但是 doc 内容直接从 cache 中获取,通过 persist_directory 方法获取 Chroma Database。
2.Similar 预测
# 检索
query = "理想汽车"
docs = db.similarity_search(query, k=5)
# 打印结果
for doc in docs:
print("===")
print("metadata:", doc.metadata)
print("page_content:", doc.page_content)
exit(0)
3.Add 添加文档
本地库存在更新慢的情况,读取缓存后如果有新的 doc 可以调用 db.add 方法添加,随后再执行查询,下面我们在 cache 的基础上引入小鹏汽车 xp.txt 的信息,并预测新的 query。
# 添加文档
text_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=0)
raw_documents_xp = TextLoader('/Users/xxx/langchain/LocalDB/xp.txt', encoding='utf-8').load()
documents_news = text_splitter.split_documents(raw_documents_xp)
db.add_documents(documents_news)
加载本地 Langchain 库后,我们可以继续将新增的本地 doc 添加至 DB 中,下面我们再测试下,这次寻找与新增小鹏汽车相关的信息:
# 检索
query = "小鹏汽车"
docs = db.similarity_search(query, k=5)
# 打印结果
for doc in docs:
print("===")
print("metadata:", doc.metadata)
print("page_content:", doc.page_content)
exit(0)
xp.txt 里小鹏汽车的关键字比较多,所以匹配下来 metadata 都指向 xp.txt,不存在之前 lx 检索到 lp 的情况:
五.总结
上面简单测试了基于 Doc 构建本地 Langchain 库的一些方法,关于更细粒度的 Langchain 和 RAG,还涉及到很多细节的点,包括对 query 的清洗与处理,对文档的清理与筛选,对 Langchain 结果的取舍与合并以及 LLM Prompt 的构建,这些细致的点大家可以一一扩散提高搜索的效果。
Tips:
Langchain 中文 API 介绍: LangChain中文网 Concepts | 🦜️🔗 Langchain