前言
本文是 Harrison Chase (LangChain 创建者)和吴恩达(Andrew Ng)的视频课程《LangChain for LLM Application Development》(使用 LangChain 进行大模型应用开发)的学习笔记。由于原课程为全英文视频课程,国内访问较慢,同时我整理和替换了部分内容以便于国内学习。阅读本文可快速学习课程内容。
课程介绍
本课程介绍了强大且易于扩展的 LangChain 框架,LangChain 框架是一款用于开发大语言模型(LLM)应用的开源框架,其使用提示词、记忆、链、代理等简化了大语言模型应用的开发工作。由于 LangChain 仍处于快速发展期,部分 API 还不稳定,课程中的部分代码已过时,我使用了目前最新的 v0.2 版本进行讲解,所有代码均可在 v0.2 版本下执行。另外,课程使用的 OpenAI 在国内难以访问,我替换为国内的 Kimi 大模型及开源自建的 Ollama,对于学习没有影响。
参考这篇文章来获取 Kimi 的 API 令牌。
参考这篇文章来用 Ollama 部署自己的大模型。
第三部分
提问和回答
生成测试数据
我们先使用任意聊天机器人来生成测试的数据,提示词如下:
请随机生成30条商品介绍数据,以逗号分隔的csv格式输出,第一行是表头,输出:no,name,description,包含以下三列:
序号
商品名称
商品的详细描述
会生成类似下面这样的内容,我们保存为 product.csv
文件。
no,name,description
1,高清智能电视,"这款高清智能电视拥有4K超高清分辨率,内置智能系统,支持语音控制,提供丰富的娱乐体验。"
2,多功能料理机,"集搅拌、打蛋、榨汁等多种功能于一身,操作简便,是厨房里的得力助手。"
3,无线蓝牙耳机,"轻巧舒适,音质清晰,支持长时间续航,适合运动和日常使用。"
4,智能扫地机器人,"自动规划清扫路线,智能避障,解放双手,保持家中清洁。"
5,便携式榨汁机,"小巧便携,操作简便,快速榨汁,适合健康生活需求。"
...
问答链
然后我们通过下面的代码,让 LLM 从我们生成的测试数据中找出和总结我们需要的数据。下面使用了 Ollama 部署的 qwen2(千问2)模型,执行前请先部署服务。
请先安装依赖 pip install docarray
from langchain_community.llms import Ollama
from langchain.chains import RetrievalQA
from langchain_community.embeddings import OllamaEmbeddings
from langchain_community.document_loaders import CSVLoader
from langchain_community.vectorstores import DocArrayInMemorySearch
from langchain.indexes import VectorstoreIndexCreator
# 服务地址
base_url = 'http://localhost:11434'
# 模型名称
llm_model = 'qwen2'
# 数据文件路径
file_path = 'product.csv'
# 创建模型
llm = Ollama(base_url=base_url, model=llm_model)
# 创建数据载入器
loader = CSVLoader(file_path=file_path)
# 创建词嵌入
embeddings = OllamaEmbeddings(base_url=base_url, model=llm_model)
# 创建向量索引
index = VectorstoreIndexCreator(
vectorstore_cls=DocArrayInMemorySearch,
embedding=embeddings
).from_loaders([loader])
# 提问
query = "请列出带有智能功能且节能环保的电器,以 Markdown 格式输出,总结它们的功能描述。"
response = index.query(query, llm=llm)
print(response)
可能会获得如下输出(由于生成的测试数据以及 LLM 每次回答不同,结果可能有较大差异)。
- **智能空调**:
* 功能描述:智能温控,自动调节,节能省电,提供舒适环境。
- **空气净化器**:
* 功能描述:高效过滤空气中的污染物,提供清新空气,适合家庭和办公室使用。
这里我们使用 LangChain 提供的问答链快速从我们自己的数据中获得了结果。这些数据是我们自己内部的数据,对于 LLM 是不具备的。
步骤分解
接下来让我们一步步分解来执行,看看上面的代码到底做了什么。
第一步是使用 CSVLoader 来载入外部数据。这些数据是不存在于 LLM 中,而是我们其他的内部数据库中,作为背景数据或内部知识库传递给 LLM。
loader = CSVLoader(file_path=file)
docs = loader.load()
print(docs[0])
可能会得到如下结果。
page_content='no: 1
name: 高清智能电视
description: 这款高清智能电视拥有4K超高清分辨率,内置智能系统,支持语音控制,提供丰富的娱乐体验。' metadata={'source': 'product.csv', 'row': 0}
这里使用了 CSVLoader,用来载入 csv 格式的数据。还有其他很多现成的 Loader,如 JSONLoader、HtmlLoader 等,也有很多其他的扩展,可以从 Office 文档、PDF、数据库、网盘中获取数据。
然后,用 Embeddings(嵌入)来处理字符串,这里我们使用 Ollama 的 embeddings 接口。我们初始化 OllamaEmbeddings 实例,调用其 embed_query 方法来创建词嵌入。
embeddings = OllamaEmbeddings(base_url=base_url, model=llm_model)
embed = embeddings.embed_query("你好,我的名字是火眼9988")
print(len(embed))
print(embed[:5])
我们可以看到词嵌入的结果是数千个数值的列表,我们可以保存这些向量数值,在后续使用。嵌入的过程就是将内容转换为向量数组。
3584
[0.46612900495529175, 3.102452516555786, -2.066977024078369, 0.17733854055404663, 1.7350211143493652]
- 嵌入的向量数组能够“保存”原先的内容或意思。
- 相似的文字也会获得相似的向量数组。
我们还可以使用向量数据库来保存这些向量数据。如果有一段比较长的文本,我们可以首先将它切割为数个短一点的片段,这样我们可以减少传递给 LLM 的内容大小。然后,我们对各个片段创建词嵌入,并将它们保存到向量数据库中。这就是我们创建索引的过程。
我们使用 from_documents 来创建向量存储。
db = DocArrayInMemorySearch.from_documents(
docs,
embeddings
)
然后,我们可以在运行时使用了。当新的请求进来时,先将请求进行嵌入处理,然后将向量和向量数据库中的所有数据进行比对,找出最相似的 n 条数据。我们就可以获得这些数据,并将它们添加到提示词中,提交给 LLM。
我们查询“智能家用电器”,让向量数据库查找最相似的数据。
query = "请选择智能家用电器"
docs = db.similarity_search(query)
print(len(docs))
print(docs[0])
会输出类似如下内容。我们看到获取了 4 条数据,第一条数据就是“智能热水器”。
4
page_content='no: 28
name: 智能热水器
description: 即开即热,智能恒温,节能环保。' metadata={'source': 'product.csv', 'row': 27}
那我们怎么使用 LLM 来查询我们自己的文档数据呢?首先,我们创建 LLM 模型,接着,将文档中的 page_content 拼接到一个字符串变量中,并将变量传递给提示词模板。最后,调用大语言模型来回答。
# 创建 Ollama LLM
llm = Ollama(base_url=base_url, model=llm_model)
# 拼接文档
qdocs = "".join([docs[i].page_content for i in range(len(docs))])
# 提问
response = llm.invoke(f"{qdocs} 问题:请找出最环保的电器?")
print(response)
可以得到类似下面的回答。
在给出的描述中,“智能热水器”因其“即开即热”和“节能环保”的特性被认为是最环保的电器。它可以在需要时快速提供热水服务,并且在设计上考虑到能源的有效利用和减少浪费,因此被评为最环保选项。
所有这些步骤都可以使用 LangChain 的链来处理。这里我们创建一条 Retrieval QA 链,我们使用下面几个参数来创建这条链。
- llm:大语言模型,这里使用的是 Ollama 的 qwen2
- chain_type:链类型,这里使用的是 stuff,是最简单的方式
- retriever:是用来获取文档的接口,它接收查询并返回文档
# 创建 retriever
retriever = db.as_retriever()
# 创建问答链
qa_stuff = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=retriever,
verbose=True
)
query = "请列出带有智能功能且节能环保的电器,以 Markdown 格式输出,总结它们的功能描述。"
response = qa_stuff.run(query)
print(response)
可得到类似下面的结果。
链的类型
上面我们在创建问答链的时候使用了 stuff 类型,具体这个有什么作用呢?又有其他哪些类型呢?
Stuff
stuff 非常简单,它将所有内容合并到一个提示词中发给 LLM,并获得一个回答。它非常简单和有效,但这里我们只有 4 个文档数据,如果我们要查询的文档数据非常庞大,我们就可以使用其他的类型。
优点:只有一次提问,LLM 可以一次获得所有的相关数据。
缺点:LLM 往往有长度限制,不适用于大批量的数据文档。
Map_reduce
Map_reduce 获取所有的文档片段,将它们和问题一起发送给 LLM,并获得回答。然后,发起另外的请求来总结之前的回答,并获得最终的回答。它可以处理任意数量的文档,并且可以并行处理所有的请求。但是,它需要发起多次请求,并且每个文档都认为是独立的,有时候可能并不会获得最佳的结果。
Refine
Refine 会逐个处理所有文档,下一个请求会基于上一个的回答。所以,它会结合多个文档之间的关联,但是它的回答会比较慢。
Map_rerank
Map_rerank 会给每个文档请求一次 LLM,并获得一个得分,然后选择最高的得分。类似于 map_reduce,每个文档也是独立的,因此它比较快速。但也会发起多个请求,会有较高的费用。
最常用的类型是 stuff,其次是 map_reduce,这个类型也可以用于其他类型的链,例如处理总结的链。
上面我们使用 LangChain 构建了一条问答链,并提供了 LLM 没有的我们自己的数据作为背景数据,这非常常用,特别是对于企业有自己的知识库的场景中。
(未完待续)