译者 | 李睿
审校 | 重楼
LangChain是当今最热门的开发平台之一,用于创建使用生成式人工智能的应用程序,但它只适用于Python和JavaScript。对于想要使用LangChain的R程序员,该怎么办?
幸运的是,可以使用非常基本的Python代码在LangChain中做很多有用的事情。而且,多亏有了reticulate R包,R和RStudio用户可以在他们熟悉的环境中编写和运行Python,包括在Python和R之间来回传递对象和数据。
在这个LangChain教程中,将展示如何使用Python和R来访问LangChain和OpenAI API。这将允许使用大型语言模型(LLM)来查询ggplot2的300页PDF文档。第一个示例查询:“如何在图形的x轴上旋转文本?”
以下是介绍这个过程的每个步骤:
(1)如果还没有运行,需要将系统设置为运行Python和reticulate。
(2)将ggplot2 PDF文档文件导入为纯文本的LangChain对象。
(3)将文本分割成可以被大型语言模型读取的更小的部分,因为这些模型对一次读取的数量有限制。而长达300页的文本将超出OpenAI公司的限制。
(4)使用LLM为每个文本块创建“嵌入”,并将它们全部保存在数据库中。嵌入是一串数字,表示多维空间中文本的语义。
(5)为用户的问题创建一个嵌入,然后将问题嵌入与文本中的所有现有问题进行比较。查找并检索最相关的文本片段。
(6)只将那些相关的文本部分输入到像GPT-3.5这样的LLM中,并要求它生成答案。
如果用户要遵循示例并使用OpenAI API,则需要一个API密钥。可以在platform.openai.com注册。如果愿意使用另一个模型,LangChain有组件可以为许多LLM构建链,而不仅仅是OpenAI公司的组件,因此用户不会被某个LLM提供商锁定。
LangChain拥有可以轻松处理这些步骤的组件,特别是如果企业对其默认值感到满意的话。这就是为什么它变得如此受欢迎的原因。
以下开始逐步实施:
步骤1:设置系统在RStudio中运行Python
如果用户已经运行了Python和reticulate,则可以跳到下一步。否则,确保在系统上有最新版本的Python。有很多方法可以安装Python,但只需要从python.org网站中下载就可以。然后,按照通常的方式使用install.packages("reticulate")安装reticulate R包。
如果已经安装了Python,但是reticulate找不到它,可以使用命令use_Python(“/path/to/your/Python”)。
library(reticulate)
virtualenv_create(envname = "langchain_env", packages = c( "langchain", "openai", "pypdf", "bs4", "python-dotenv", "chromadb", "tiktoken")) # Only do this once
注意,用户可以随意命名其环境。如果需要在创建环境后安装软件包,使用py_install(),如下所示:
py_install(packages = c( "langchain", "openai", "pypdf", "bs4", "python-dotenv", "chromadb", "tiktoken"), envname = "langchain_env")
与在R中一样,用户应该只需要安装一次软件包,而不是每次需要使用环境时都安装软件包。另外,不要忘记激活虚拟环境:
use_virtualenv("langchain_env")
每次回到项目时,在开始运行Python代码之前,都要这样做。
用户可以测试其Python环境是否正在使用
reticulate::py_run_string('
print("Hello, world!")
')
如果用户喜欢的话,可以采用Python变量设置OpenAI API密钥。因为已经在一个R环境变量中有了它,通常使用R来设置OpenAI API密钥。用户可以使用reticulate的r_to_py()函数将任何R变量保存为python友好的格式,包括环境变量:
api_key_for_py <- r_to_py(Sys.getenv("OPENAI_API_KEY"))
它接受OPENAI_API_KEY环境变量,确保它是Python友好的,并将其存储在一个新变量中:api_key_for_py(同样,可以采用任何名称)。
最后,准备好编写代码!
步骤2:下载并导入PDF文件
将在主项目目录下创建一个新的docs子目录,并使用R在那里下载文件。
# Create the directory if it doesn't exist
if(!(dir.exists("docs"))) {
dir.create("docs")
}
# Download the file
download.file("https://cran.r-project.org/web/packages/ggplot2/ggplot2.pdf", destfile = "docs/ggplot2.pdf", mode = "wb")
接下来是Python代码,将该文件导入为包含内容和元数据的LangChain文档对象。将为此创建一个名为prep_docs.py的新Python脚本文件。可以像上面那样使用py_run_string()函数在R脚本中继续运行Python代码。然而,如果用户正在处理一个更大的任务,那么就不太理想,因为将会在诸如代码完成之类的事项上面临失败。
Python新手的关键点:不要将脚本文件的名称与将要加载的Python模块的名称相同!换句话说,虽然该文件不必命名为prep_docs.py,但如果要导入langchain包,就不要将其命名为langchain.py !它们会发生冲突。这在R中并不是问题。
以下是新的prep_docs.py文件的第一部分:
If running from RStudio, remember to first run in R:
# library(reticulate)
# use_virtualenv("the_virtual_environment_you_set_up")
# api_key_py <- r_to_py(Sys.getenv("OPENAI_API_KEY"))
from langchain.document_loaders import PyPDFLoader
my_loader = PyPDFLoader('docs/ggplot2.pdf')
# print(type (my_loader))
all_pages = my_loader.load()
# print(type(all_pages))
print( len(all_pages) )
这段代码首先导入PDF文档加载器PyPDFLoader。接下来,它创建PDF加载器类的一个实例。然后,它运行加载器及其load方法,将结果存储在一个名为all_pages的变量中。该对象是一个Python列表。
在这里包含了一些注释行,如果想看到它们,它们将打印对象类型。最后一行打印列表的长度,在本例中是304。
可以点击RStudio中的source按钮来运行一个完整的Python脚本。或者,突出显示一些代码行并只运行它们,就像使用R脚本一样。Python代码在运行时看起来与R代码略有不同,因为它在R控制台中打开一个Python交互式REPL会话。用户将被指示输入exit或quit(没有括号)以退出并在完成后返回常规R控制台。
用户可以使用reticulate的py对象在R中检查all_pages Python对象。下面的R代码将Python all_pages对象存储到一个名为all_pages_in_r的R变量中(用户可以随意调用它)。然后,可以像处理任何其他R对象一样处理该对象。在本例中,它是一个列表。
all_pages_in_r <- py$all_pages
# Examples:
all_pages_in_r[[1]]$metadata # See metadata in the first item
nchar(all_pages_in_r[[100]]$page_content) # Count number of characters in the 100th item
LangChain集成
如果用户还没有最喜欢的将PDF转换为可读文本的方法,那么LangChain的PyPDFLoader可以方便地用于其他非人工智能项目。而且,LangChain还有100多种其他文件加载器,包括PowerPoint、Word、网页、YouTube、epub、Evernote和Notion等格式。可以在LangChain集成中心中看到一些文件格式和集成文档加载器。
步骤3:将文档拆分为多个部分
LangChain有几个转换器可以将文档分解成块,包括按字符、标记和标记头进行拆分。一个推荐的默认值是RecursiveCharacterTextSplitter,它将“递归地尝试按不同的字符进行拆分,以找到一个有效的字符”。另一个流行的选项是CharacterTextSplitter,它的设计目的是让用户设置其参数。
用户可以设置该拆分器的最大文本块大小,是按字符计数还是按LLM令牌计数(令牌通常是1到4个字符),以及文本块应该重叠多少。在开始使用LangChain之前,从未考虑过文本块重叠的必要性,但它是有意义的,除非用户可以通过逻辑块(如用标题分隔的章节或节)来分隔。否则,文本可能会在句子中间被拆分,一个重要的信息可能会被分成两个部分,其中任何一个都没有明确的完整含义。
用户还可以选择希望拆分器在分割文本时优先考虑哪些分隔符。CharacterTextSplitter的默认值是首先拆分为两个新行(\n\n),然后再拆分一个新行、一个空格,最后完全不使用分隔符。
下面的代码通过使用Python内部的reticulate的R对象,从R api_key_for_py变量导入OpenAI API密钥。它还加载openai Python包和LangChain的递归字符分割器,创建一个RecursiveCharacterTextSplitter类的实例,并在all_pages块上运行该实例的split_documents()方法。
import openai
openai.api_key = r.api_key_for_py
from langchain.text_splitter import RecursiveCharacterTextSplitter
my_doc_splitter_recursive = RecursiveCharacterTextSplitter()
my_split_docs = my_doc_splitter_recursive.split_documents(all_pages)
同样,用户可以用R代码将这些结果发送给R,例如:
My_split_docs <- py$ My_split_docs
是否想知道块中的最大字符数是多少?可以用R中的一个自定义函数来检查这个列表:
get_characters <- function(the_chunk) {
x <- nchar(the_chunk$page_content)
return(x)
}
purrr::map_int(my_split_docs, get_characters) |>
max()
这将生成3,985个字符,因此看起来默认的块最大值是4,000个字符。
如果想要更小的文本块,首先尝试CharacterTextSplitter并人工地将chunk_size设置为小于4,000,例如
chunk_size = 1000
chunk_overlap = 150
from langchain.text_splitter import CharacterTextSplitter
c_splitter = CharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap, separator=" ")
c_split_docs = c_splitter.split_documents(all_pages)
print(len(c_split_docs)) # To see in Python how many chunks there are now
可以在R和Python中检查结果:
c_split_docs <- py$c_split_docs
length(c_split_docs)
该代码生成695个块,最大为1000。
成本是多少?
在进一步讨论之前,如果想知道为所有这些块生成嵌入是否会非常昂贵。将从默认的306项递归拆分开始。可以计算R对象上这些块中的字符总数:
purrr::map_int(my_split_docs, get_characters) |>
sum()
答案是513506。保守地估计每个令牌有两个字符,其结果大约是20万个。
如果想更准确,TheOpenAIR R包有一个count_tokens()函数(确保安装该函数和purrr以使用下面的R代码):
purrr::map_int(my_split_docs, ~ TheOpenAIR::count_tokens(.x$page_content)) |>
sum ()
该代码显示了126,343个令牌。
那要花费多少成本?OpenAI用于生成嵌入的模型是ada-2。现在ada-2的1000个令牌价格为0.0001美元,126,000代币的价格约为1.3美分。这些费用在预算之内。
步骤4:生成嵌入
LangChain有预制的组件,可以从文本块创建嵌入并存储它们。对于存储,将使用LangChain中最简单的选项之一:Chroma,这是一个可以在本地使用的开源嵌入数据库。
首先,将用R代码为docs目录创建一个子目录,因为建议在Chroma目录中除了数据库之外什么都不要。这是R代码:
if(!dir.exists("docs/chroma_db")) {
dir.create("docs/chromaba_db")
}
下面是使用LangChain的OpenAIEmbeddings生成嵌入的一些Python代码。这是目前默认为OpenAI的ada-2模型,因此不需要指定它。LangChain通过其嵌入类支持许多其他LLM,包括Hugging Face Hub、Cohere、Llama cpp和Spacy。
下面的Python代码稍微修改了一下DeepLearning.AI的LangChain与其数据聊天在线教程。
from langchain.embeddings.openai import OpenAIEmbeddings
embed_object = OpenAIEmbeddings()
from langchain.vectorstores import Chroma
chroma_store_directory = "docs/chroma_db"
vectordb = Chroma.from_documents(
documents=my_split_docs,
embedding=embed_object,
persist_directory=chroma_store_directory
)
# Check how many embeddings were created
print(vectordb._collection.count())
注意_collection.count()中的下划线!
可以看到有306个嵌入,与ggplot2文本块的数量相同。
Python新手的另一个注意事项:缩进在Python中很重要。确保非缩进行之前没有空格,并且缩进行都使用相同数量的缩进空格。
在这个系统上,这段代码似乎将数据保存到了磁盘上。但是,教程指出用户应该运行以下Python代码来保存嵌入以供以后使用。这样做的原因是不想在文档更改之前重新生成嵌入。
vectordb.persist()
现在,已经完成了为查询准备文档的工作。将创建qanda.py这个新文件,来使用创建的矢量嵌入。
步骤5:嵌入用户查询和查找文档块
现在是时候提出一个问题,为该问题生成嵌入,并根据块的嵌入检索与该问题最相关的文档。
由于vectordb对象的内置方法,LangChain提供了在一行代码中完成所有这些工作的几种方法。它的similarity_search()方法直接计算向量相似度,并返回最相似的文本块。
不过,还有其他几种方法可以做到这一点,包括max_marginal_relevance e_search()。这背后的想法是,不一定想要三个几乎相同的文本块。如果文本中有一点多样性,以获得额外的有用信息,也许最终会得到一个更丰富的回答。因此,max_marginal_relevance e_search()检索的相关文本比实际计划传递给LLM以获取答案的文本多一些(用户决定多出多少)。然后,结合一定程度的多样性,它选择最后的文本片段。
用户可以指定希望similarity_search()返回多少相关文本块及其k参数。对于max_marginal_relevance(),用户指定最初应该使用fetch_k检索多少块,以及希望LLM查找其使用k的答案的最终文本片段。
如果文档没有更改,不想运行文档准备文件,将首先在新的qanda.py文件中加载必要的包和环境变量(即OpenAI API密钥),就像在使用doc_prepare .py之前所做的那样。然后,将加载chromadb矢量数据库:
# If running from RStudio, remember to first run in R:
# library(reticulate)
# use_virtualenv("the_virtual_environment_you_set_up")
# api_key_py <- r_to_py(Sys.getenv("OPENAI_API_KEY"))
import openai
openai.api_key = r.api_key_for_py
from langchain.embeddings.openai import OpenAIEmbeddings
embed_object = OpenAIEmbeddings()
from langchain.vectorstores import Chroma
chroma_store_directory = "docs/chroma_db"
vectordb = Chroma(persist_directory=chroma_store_directory,
embedding_functinotallow=embed_object)
接下来,将硬编码一个问题并检索相关文档。需要注意,可以用一行代码检索文档:
my_question = "How do you rotate text on the x-axis of a graph?"
# For straightforward similarity searching
sim_docs = vectordb.similarity_search(my_question)
# For maximum marginal relevance search retrieving 5 possible chunks and choosing 3 finalists:
mm_docs = vectordb.max_marginal_relevance_search(my_question, k = 3, fetch_k = 5)
如果想查看检索到的文档片段,可以在Python中打印它们,如下所示:
for doc in mm_docs:
print(doc.page_content)
for doc in sim_docs:
print(doc.page_content)
注意缩进是for循环的一部分。
还可以使用以下命令查看它们的元数据:
for doc in mm_docs:
print(doc.metadata)
for docs in sim_docs:
print(docs.metadata)
与其他对象一样,也可以在R中查看这些对象:
mm_relevant <- py$mm_docs
sim_relevant <- py$sim_docs
不确定为什么当请求三个文档时,模型有时会返回四个文档,但这应该不是问题,除非LLM在遍历文本以生成响应时有太多的令牌。
步骤6:生成答案
现在是时候让GPT-3.5这样的LLM根据相关文档生成对用户问题的书面回复了。可以使用LangChain的RetrievalQA功能来实现这一点。
建议首先尝试LangChain的默认模板,这很容易实现,通常适用于原型制作或用户自己使用:
# Set up the LLM you want to use, in this example OpenAI's gpt-3.5-turbo
from langchain.chat_models import ChatOpenAI
the_llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)
# Create a chain using the RetrievalQA component
from langchain.chains import RetrievalQA
qa_chain = RetrievalQA.from_chain_type(the_llm,retriever=vectordb.as_retriever())
# Run the chain on the question, and print the result
print(qa_chain.run(my_question))
LLM给出了以下回复:
To rotate text on the x-axis of a graph, you can use the `theme()` function in ggplot2. Specifically, you can use the `axis.text.x` argument to modify the appearance of the x-axis text. Here is an example:
```R
library(ggplot2)
# Create a basic scatter plot
p <- ggplot(mtcars, aes(x = mpg, y = wt)) +
geom_point()
# Rotate x-axis text by 45 degrees
p + theme(axis.text.x = element_text(angle = 45, hjust = 1))
```
In this example, the `angle` argument is set to 45, which rotates the x-axis text by 45 degrees. The `hjust` argument is set to 1, which aligns the text to the right. You can adjust the angle and alignment values to achieve the desired rotation and alignment of the x-axis text.
看起来是正确的!
现在链已经设置好了,可以用一个R脚本用一个命令在其他问题上运行它:
py_run_string('
print(qa_chain.run("How can I make a bar chart where the bars are steel blue?"))
')
以下是它们的回应:
```R
library(ggplot2)
# Create a bar chart with steel blue bars
p <- ggplot(mtcars, aes(factor(cyl)))
p + geom_bar(fill = "steelblue")
```
In this example, we use the `fill` aesthetic to specify the color of the bars as "steelblue". You can adjust the color to your preference by changing the color name or using a hexadecimal color code.
这是一个比在ChatGPT 3.5中有时收到的相同问题可获得更好的答案。有时它发回的代码实际上并不起作用。
用户可能还想确认答案不是从一般的ChatGPT知识库中提取的,而是真正来自其上传的文档。为了找到答案,可以问一些与ggplot2完全无关的问题,这些问题不会出现在文档中:
py_run_string('
print(qa_chain.run("What is the capital of Australia?"))
')
用户应该这样回复:
I don't know.
如果正在创建一个应用程序以供更广泛的使用,那么“我不知道”可能有点简洁。如果用户想定制默认模板,可以查看LangChain文档。如果用户正在为不止自己或一个小团队创建应用程序,那么个性化响应是有意义的。
模板调整是LangChain可能感觉过于复杂的一个领域,它可能需要多行代码来实现对模板的小更改。然而,使用任何固执己见的框架都是有风险的,这取决于每个开发人员来决定项目的总体收益是否值得这样的成本。虽然它非常受欢迎,但并不是每个人都是LangChain的忠实用户。
还可以用LangChain做什么?
到目前为止,对应用程序最简单的添加是包含更多文档。LangChain有一个DirectoryLoader来简化这个过程。如果用户正在跨多个文档进行搜索,可能希望知道哪些文档用于生成响应。可以给RetrievalQA添加return_source_documents=True参数,如下所示:
qa_chain = RetrievalQA.from_chain_type(the_llm,retriever=vectordb.as_retriever(), return_source_documents=True)
my_result = qa_chain({"query": my_question})
print(my_result['result'])
该代码最初只对单个用户在本地运行有用,但它可以成为使用Streamlit或Shiny for Python等框架的交互式Web应用程序的逻辑基础。或者,将Python和R结合起来,将LLM的最终答案发送回R,并使用Shiny R Web框架创建一个应用程序(尽管发现同时使用Python和R部署Shiny应用程序有点复杂)。
还要注意的是,这个应用程序在技术上并不是一个“聊天机器人”,因为它不会记住用户之前的问题。所以,不能有一个“对话”,例如“如何改变图表标题文字的大小?”,然后是“图例呢?”用户需要把每个新单词拼出来。
但是,可以向应用程序添加内存,使用LangChain的ConversationBufferMemory将其转换为聊天机器人。
其他资源
要了解更多关于LangChain的信息,除了LangChain文档之外,还有一个LangChain Discord服务器,其中有一个人工智能聊天机器人kapa。它可以查询文档。
原文标题:Generative AI with LangChain, RStudio, and just enough Python,作者:Sharon Machlis