今天给大家来点纯正干货,看完这个系列指南后,什么构建一个自己知识库的问答机器、让你的 OpenAI API 联网搜索并给出回答、总结 PDF 文档、基于某个 Youtube 视频进行问答 ... 等等的功能都不在话下了。是不是想想都有点小兴奋,话不多说,我们马上开始。
介绍
LangChain
文档地址:https://python.langchain.com/en/latest/
这个库目前非常活跃,每天都在迭代,已经有 22k 的 star,更新速度飞快。
LangChain 是一个用于开发由语言模型驱动的应用程序的框架。他主要拥有 2 个能力:
可以将 LLM 模型与外部数据源进行连接 允许与 LLM 模型进行交互
LLM 模型:Large Language Model,大型语言模型
基础功能
LLM 调用
支持多种模型接口,比如 OpenAI、HuggingFace、AzureOpenAI ... Fake LLM,用于测试 缓存的支持,比如 in-mem(内存)、SQLite、Redis、SQL 用量记录 支持流模式(就是一个字一个字的返回,类似打字效果)
Prompt管理,支持各种自定义模板
拥有大量的文档加载器,比如 Email、Markdown、PDF、Youtube ...
对索引的支持
文档分割器 向量化 对接向量存储与搜索,比如 Chroma、Pinecone、Qdrand
Chains
LLMChain 各种工具Chain LangChainHub
必知概念
相信大家看完上面的介绍多半会一脸懵逼。不要担心,上面的概念其实在刚开始学的时候不是很重要,当我们讲完后面的例子之后,在回来看上面的内容会一下明白很多。
但是,这里有几个概念是必须知道的。
Loader 加载器
DirectoryLoaderAzureBlobStorageContainerLoaderCSVLoaderEverNoteLoaderGoogleDriveLoaderUnstructuredHTMLLoaderPyPDFLoaderS3DirectoryLoaderS3FileLoader
YoutubeLoader
https://python.langchain.com/en/latest/modules/indexes/document_loaders/examples/youtube.html
Document 文档
当使用loader加载器读取到数据源后,数据源需要转换成 Document 对象后,后续才能进行使用。
Text Spltters 文本分割
顾名思义,文本分割就是用来分割文本的。为什么需要分割文本?因为我们每次不管是做把文本当作 prompt 发给 openai api ,还是还是使用 openai api embedding 功能都是有字符限制的。
比如我们将一份300页的 pdf 发给 openai api,让他进行总结,他肯定会报超过最大 Token 错。所以这里就需要使用文本分割器去分割我们 loader 进来的 Document。
Vectorstores 向量数据库
Document
官方也提供了很多的向量数据库供我们使用。
https://python.langchain.com/en/latest/modules/indexes/vectorstores.html
Chain 链
我们可以把 Chain 理解为任务。一个 Chain 就是一个任务,当然也可以像链条一样,一个一个的执行多个链。
Agent 代理
我们可以简单的理解为他可以动态的帮我们选择和调用chain或者已有的工具。
执行过程可以参考下面这张图:
Embedding
用于衡量文本的相关性。这个也是 OpenAI API 能实现构建自己知识库的关键所在。
他相比 fine-tuning 最大的优势就是,不用进行训练,并且可以实时添加新的内容,而不用加一次新的内容就训练一次,并且各方面成本要比 fine-tuning 低很多。
具体比较和选择可以参考这个视频:https://www.youtube.com/watch?v=9qq6HTr7Ocw
实战
通过上面的必备概念大家应该已经可以对 LangChain 有了一定的了解,但是可能还有有些懵。
这都是小问题,我相信看完后面的实战,你们就会彻底的理解上面的内容,并且能感受到这个库的真正强大之处。
因为我们 OpenAI API 进阶,所以我们后面的范例使用的 LLM 都是以Open AI 为例,后面大家可以根据自己任务的需要换成自己需要的 LLM 模型即可。
当然,在这篇文章的末尾,全部的全部代码都会被保存为一个 colab 的 ipynb 文件提供给大家来学习。
建议大家按顺序去看每个例子,因为下一个例子会用到上一个例子里面的知识点。
当然,如果有看不懂的也不用担心,可以继续往后看,第一次学习讲究的是不求甚解。
完成一次问答
第一个案例,我们就来个最简单的,用 LangChain 加载 OpenAI 的模型,并且完成一次问答。
在开始之前,我们需要先设置我们的 openai 的 key,这个 key 可以在用户管理里面创建,这里就不细说了。
import os
os.environ["OPENAI_API_KEY"] = '你的api key'
然后,我们进行导入和执行
from langchain.llms import OpenAI
llm = OpenAI(model_name="text-davinci-003",max_tokens=1024)
llm("怎么评价人工智能")
这时,我们就可以看到他给我们的返回结果了,怎么样,是不是很简单。
通过 Google 搜索并返回答案
接下来,我们就来搞点有意思的。我们来让我们的 OpenAI api 联网搜索,并返回答案给我们。
这里我们需要借助 Serpapi 来进行实现,Serpapi 提供了 google 搜索的 api 接口。
首先需要我们到 Serpapi 官网上注册一个用户,https://serpapi.com/ 并复制他给我们生成 api key。
然后我们需要像上面的 openai api key 一样设置到环境变量里面去。
import os
os.environ["OPENAI_API_KEY"] = '你的api key'
os.environ["SERPAPI_API_KEY"] = '你的api key'
然后,开始编写我的代码
from langchain.agents import load_tools
from langchain.agents import initialize_agent
from langchain.llms import OpenAI
from langchain.agents import AgentType
# 加载 OpenAI 模型
llm = OpenAI(temperature=0,max_tokens=2048)
# 加载 serpapi 工具
tools = load_tools(["serpapi"])
# 如果搜索完想在计算一下可以这么写
# tools = load_tools(['serpapi', 'llm-math'])
# 如果搜索完想再让他再用python的print做点简单的计算,可以这样写
# tools=load_tools(["serpapi","python_repl"])
# 工具加载后都需要初始化,verbose 参数为 True,会打印全部的执行详情
agent = initialize_agent(tools, llm, agent=AgentType.SELF_ASK_WITH_SEARCH, verbose=True)
# 运行 agent
agent.run("What's the date today? What great events have taken place today in history?")
我们可以看到,他正确的返回了日期(有时差),并且返回了历史上的今天。
verbose
可以在上面返回的结果看到,他将我们的问题拆分成了几个步骤,然后一步一步得到最终的答案。
关于agent type 几个选项的含义(理解不了也不会影响下面的学习,用多了自然理解了):
zero-shot-react-description: 根据工具的描述和请求内容的来决定使用哪个工具(最常用)
SearchLookupWipipediaGoogle search API
reAct 介绍可以看这个:https://arxiv.org/pdf/2210.03629.pdf
agent type 官方解释:
https://python.langchain.com/en/latest/modules/agents/agents/agent_types.html?highlight=zero-shot-react-description
serpapi
ChatGPT Plugins
不过目前只能使用不用授权的插件,期待未来官方解决这个。
感兴趣的可以看这个文档:https://python.langchain.com/en/latest/modules/agents/tools/examples/chatgpt_plugins.html
Chatgpt 只能给官方赚钱,而 Openai API 能给我赚钱
对超长文本进行总结
假如我们想要用 openai api 对一个段文本进行总结,我们通常的做法就是直接发给 api 让他总结。但是如果文本超过了 api 最大的 token 限制就会报错。
这时,我们一般会进行对文章进行分段,比如通过 tiktoken 计算并分割,然后将各段发送给 api 进行总结,最后将各段的总结再进行一个全部的总结。
如果,你用是 LangChain,他很好的帮我们处理了这个过程,使得我们编写代码变的非常简单。
废话不多说,直接上代码。
from langchain.document_loaders import UnstructuredFileLoader
from langchain.chains.summarize import load_summarize_chain
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain import OpenAI
# 导入文本
loader = UnstructuredFileLoader("/content/sample_data/data/lg_test.txt")
# 将文本转成 Document 对象
document = loader.load()
print(f'documents:{len(document)}')
# 初始化文本分割器
text_splitter = RecursiveCharacterTextSplitter(
chunk_size = 500,
chunk_overlap = 0
)
# 切分文本
split_documents = text_splitter.split_documents(document)
print(f'documents:{len(split_documents)}')
# 加载 llm 模型
llm = OpenAI(model_name="text-davinci-003", max_tokens=1500)
# 创建总结链
chain = load_summarize_chain(llm, chain_type="refine", verbose=True)
# 执行总结链,(为了快速演示,只总结前5段)
chain.run(split_documents[:5])
首先我们对切割前和切割后的 document 个数进行了打印,我们可以看到,切割前就是只有整篇的一个 document,切割完成后,会把上面一个 document 切成 317 个 document。
最终输出了对前 5 个 document 的总结。
这里有几个参数需要注意:
chunk_overlap
chunk_overlap=0chunk_overlap=2
不过,这个也不是绝对的,要看所使用的那个文本分割模型内部的具体算法。
文本分割器可以参考这个文档:https://python.langchain.com/en/latest/modules/indexes/text_splitters.html
chain_type
这个参数主要控制了将 document 传递给 llm 模型的方式,一共有 4 种方式:
stuff
map_reduce
refine
map_rerank
构建本地知识库问答机器人
在这个例子中,我们会介绍如何从我们本地读取多个文档构建知识库,并且使用 Openai API 在知识库中进行搜索并给出答案。
这个是个很有用的教程,比如可以很方便的做一个可以介绍公司业务的机器人,或是介绍一个产品的机器人。
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.text_splitter import CharacterTextSplitter
from langchain import OpenAI,VectorDBQA
from langchain.document_loaders import DirectoryLoader
from langchain.chains import RetrievalQA
# 加载文件夹中的所有txt类型的文件
loader = DirectoryLoader('/content/sample_data/data/', glob='**/*.txt')
# 将数据转成 document 对象,每个文件会作为一个 document
documents = loader.load()
# 初始化加载器
text_splitter = CharacterTextSplitter(chunk_size=100, chunk_overlap=0)
# 切割加载的 document
split_docs = text_splitter.split_documents(documents)
# 初始化 openai 的 embeddings 对象
embeddings = OpenAIEmbeddings()
# 将 document 通过 openai 的 embeddings 对象计算 embedding 向量信息并临时存入 Chroma 向量数据库,用于后续匹配查询
docsearch = Chroma.from_documents(split_docs, embeddings)
# 创建问答对象
qa = VectorDBQA.from_chain_type(llm=OpenAI(), chain_type="stuff", vectorstore=docsearch,return_source_documents=True)
# 进行问答
result = qa({"query": "科大讯飞今年第一季度收入是多少?"})
print(result)
我们可以通过结果看到,他成功的从我们的给到的数据中获取了正确的答案。
关于 Openai embeddings 详细资料可以参看这个连接: https://platform.openai.com/docs/guides/embeddings
构建向量索引数据库
我们上个案例里面有一步是将 document 信息转换成向量信息和embeddings的信息并临时存入 Chroma 数据库。
因为是临时存入,所以当我们上面的代码执行完成后,上面的向量化后的数据将会丢失。如果想下次使用,那么就还需要再计算一次embeddings,这肯定不是我们想要的。
那么,这个案例我们就来通过 Chroma 和 Pinecone 这两个数据库来讲一下如何做向量数据持久化。
因为 LangChain 支持的数据库有很多,所以这里就介绍两个用的比较多的,更多的可以参看文档:https://python.langchain.com/en/latest/modules/indexes/vectorstores/getting_started.html
Chroma
persist_directoryfrom_document
from langchain.vectorstores import Chroma
# 持久化数据
docsearch = Chroma.from_documents(documents, embeddings, persist_directory="D:/vector_store")
docsearch.persist()
# 加载数据
docsearch = Chroma(persist_directory="D:/vector_store", embedding_function=embeddings)
Pinecone
Pinecone 是一个在线的向量数据库。所以,我可以第一步依旧是注册,然后拿到对应的 api key。https://app.pinecone.io/
免费版如果索引14天不使用会被自动清除。
然后创建我们的数据库:
Index Name:这个随意
Dimensions:OpenAI 的 text-embedding-ada-002 模型为 OUTPUT DIMENSIONS 为 1536,所以我们这里填 1536
Metric:可以默认为 cosine
Pod Type:想好就用 S1,想快就用 P1
持久化数据和加载数据代码如下
# 持久化数据
docsearch = Pinecone.from_texts([t.page_content for t in split_docs], embeddings, index_name=index_name)
# 加载数据
docsearch = Pinecone.from_existing_index(index_name, embeddings)
一个简单从数据库获取 embeddings,并回答的代码如下
from langchain.text_splitter import CharacterTextSplitter
from langchain.document_loaders import DirectoryLoader
from langchain.vectorstores import Chroma, Pinecone
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.llms import OpenAI
from langchain.chains.question_answering import load_qa_chain
import pinecone
# 初始化 pinecone
pinecone.init(
api_key="你的api key",
environment="你的Environment"
)
loader = DirectoryLoader('/content/sample_data/data/', glob='**/*.txt')
# 将数据转成 document 对象,每个文件会作为一个 document
documents = loader.load()
# 初始化加载器
text_splitter = CharacterTextSplitter(chunk_size=500, chunk_overlap=0)
# 切割加载的 document
split_docs = text_splitter.split_documents(documents)
index_name="liaokong-test"
# 持久化数据
# docsearch = Pinecone.from_texts([t.page_content for t in split_docs], embeddings, index_name=index_name)
# 加载数据
docsearch = Pinecone.from_existing_index(index_name,embeddings)
query = "科大讯飞今年第一季度收入是多少?"
docs = docsearch.similarity_search(query, include_metadata=True)
llm = OpenAI(temperature=0)
chain = load_qa_chain(llm, chain_type="stuff", verbose=True)
chain.run(input_documents=docs, question=query)
使用GPT3.5模型构建油管频道问答机器人
在 chatgpt api(也就是 GPT-3.5-Turbo)模型出来后,因钱少活好深受大家喜爱,所以 LangChain 也加入了专属的链和模型,我们来跟着这个例子看下如何使用他。
import os
from langchain.document_loaders import YoutubeLoader
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains import ChatVectorDBChain, ConversationalRetrievalChain
from langchain.chat_models import ChatOpenAI
from langchain.prompts.chat import (
ChatPromptTemplate,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate
)
# 加载 youtube 频道
loader = YoutubeLoader.from_youtube_channel('https://www.youtube.com/watch?v=Dj60HHy-Kqk')
# 将数据转成 document
documents = loader.load()
# 初始化文本分割器
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=20
)
# 分割 youtube documents
documents = text_splitter.split_documents(documents)
# 初始化 openai embeddings
embeddings = OpenAIEmbeddings()
# 将数据存入向量存储
vector_store = Chroma.from_documents(documents, embeddings)
# 通过向量存储初始化检索器
retriever = vector_store.as_retriever()
system_template = """
Use the following context to answer the user's question.
If you don't know the answer, say you don't, don't try to make it up. And answer in Chinese.
-----------
{context}
-----------
{chat_history}
"""
# 构建初始 messages 列表,这里可以理解为是 openai 传入的 messages 参数
messages = [
SystemMessagePromptTemplate.from_template(system_template),
HumanMessagePromptTemplate.from_template('{question}')
]
# 初始化 prompt 对象
prompt = ChatPromptTemplate.from_messages(messages)
# 初始化问答链
qa = ConversationalRetrievalChain.from_llm(ChatOpenAI(temperature=0.1,max_tokens=2048),retriever,qa_prompt=prompt)
chat_history = []
while True:
question = input('问题:')
# 开始发送问题 chat_history 为必须参数,用于存储对话历史
result = qa({'question': question, 'chat_history': chat_history})
chat_history.append((question, result['answer']))
print(result['answer'])
我们可以看到他能很准确的围绕这个油管视频进行问答
使用流式回答也很方便
from langchain.callbacks.base import CallbackManager
from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
chat = ChatOpenAI(streaming=True, callback_manager=CallbackManager([StreamingStdOutCallbackHandler()]), verbose=True, temperature=0)
resp = chat(chat_prompt_with_values.to_messages())
用 OpenAI 连接万种工具
zapier
所以我们第一步依旧是需要申请账号和他的自然语言 api key。https://zapier.com/l/natural-language-actions
他的 api key 虽然需要填写信息申请。但是基本填入信息后,基本可以秒在邮箱里看到审核通过的邮件。
Manage Actions
我在这里配置了 Gmail 读取和发邮件的 action,并且所有字段都选的是通过 AI 猜。
配置好后,我们开始写代码
import os
os.environ["ZAPIER_NLA_API_KEY"] = ''
from langchain.llms import OpenAI
from langchain.agents import initialize_agent
from langchain.agents.agent_toolkits import ZapierToolkit
from langchain.utilities.zapier import ZapierNLAWrapper
llm = OpenAI(temperature=.3)
zapier = ZapierNLAWrapper()
toolkit = ZapierToolkit.from_zapier_nla_wrapper(zapier)
agent = initialize_agent(toolkit.get_tools(), llm, agent="zero-shot-react-description", verbose=True)
# 我们可以通过打印的方式看到我们都在 Zapier 里面配置了哪些可以用的工具
for tool in toolkit.get_tools():
print (tool.name)
print (tool.description)
print ("\n\n")
agent.run('请用中文总结最后一封"******@qq.com"发给我的邮件。并将总结发送给"******@qq.com"')
******@qq.com******@qq.com
这是我发送给 Gmail 的邮件。
这是他发送给 QQ 邮箱的邮件。
zapier
一些有意思的小Tip
执行多个chain
因为他是链式的,所以他也可以按顺序依次去执行多个 chain
from langchain.llms import OpenAI
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain.chains import SimpleSequentialChain
# location 链
llm = OpenAI(temperature=1)
template = """Your job is to come up with a classic dish from the area that the users suggests.
% USER LOCATION
{user_location}
YOUR RESPONSE:
"""
prompt_template = PromptTemplate(input_variables=["user_location"], template=template)
location_chain = LLMChain(llm=llm, prompt=prompt_template)
# meal 链
template = """Given a meal, give a short and simple recipe on how to make that dish at home.
% MEAL
{user_meal}
YOUR RESPONSE:
"""
prompt_template = PromptTemplate(input_variables=["user_meal"], template=template)
meal_chain = LLMChain(llm=llm, prompt=prompt_template)
# 通过 SimpleSequentialChain 串联起来,第一个答案会被替换第二个中的user_meal,然后再进行询问
overall_chain = SimpleSequentialChain(chains=[location_chain, meal_chain], verbose=True)
review = overall_chain.run("Rome")
结构化输出
有时候我们希望输出的内容不是文本,而是像 json 那样结构化的数据。
from langchain.output_parsers import StructuredOutputParser, ResponseSchema
from langchain.prompts import PromptTemplate
from langchain.llms import OpenAI
llm = OpenAI(model_name="text-davinci-003")
# 告诉他我们生成的内容需要哪些字段,每个字段类型式啥
response_schemas = [
ResponseSchema(name="bad_string", description="This a poorly formatted user input string"),
ResponseSchema(name="good_string", description="This is your response, a reformatted response")
]
# 初始化解析器
output_parser = StructuredOutputParser.from_response_schemas(response_schemas)
# 生成的格式提示符
# {
# "bad_string": string // This a poorly formatted user input string
# "good_string": string // This is your response, a reformatted response
#}
format_instructions = output_parser.get_format_instructions()
template = """
You will be given a poorly formatted string from a user.
Reformat it and make sure all the words are spelled correctly
{format_instructions}
% USER INPUT:
{user_input}
YOUR RESPONSE:
"""
# 讲我们的格式描述嵌入到 prompt 中去,告诉 llm 我们需要他输出什么样格式的内容
prompt = PromptTemplate(
input_variables=["user_input"],
partial_variables={"format_instructions": format_instructions},
template=template
)
promptValue = prompt.format(user_input="welcom to califonya!")
llm_output = llm(promptValue)
# 使用解析器进行解析生成的内容
output_parser.parse(llm_output)
自定义agent中所使用的工具
from langchain.agents import initialize_agent, Tool
from langchain.agents import AgentType
from langchain.tools import BaseTool
from langchain.llms import OpenAI
from langchain import LLMMathChain, SerpAPIWrapper
llm = OpenAI(temperature=0)
# 初始化搜索链和计算链
search = SerpAPIWrapper()
llm_math_chain = LLMMathChain(llm=llm, verbose=True)
# 创建一个功能列表,指明这个 agent 里面都有哪些可用工具,agent 执行过程可以看必知概念里的 Agent 那张图
tools = [
Tool(
name = "Search",
func=search.run,
description="useful for when you need to answer questions about current events"
),
Tool(
name="Calculator",
func=llm_math_chain.run,
description="useful for when you need to answer questions about math"
)
]
# 初始化 agent
agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True)
# 执行 agent
agent.run("Who is Leo DiCaprio's girlfriend? What is her current age raised to the 0.43 power?")
工具中描述内容
比如 Calculator 在描述里面写到,如果你问关于数学的问题就用他这个工具。我们就可以在上面的执行过程中看到,他在我们请求的 prompt 中数学的部分,就选用了Calculator 这个工具进行计算。
使用Memory实现一个带记忆的对话机器人
上一个例子我们使用的是通过自定义一个列表来存储对话的方式来保存历史的。
当然,你也可以使用自带的 memory 对象来实现这一点。
from langchain.memory import ChatMessageHistory
from langchain.chat_models import ChatOpenAI
chat = ChatOpenAI(temperature=0)
# 初始化 MessageHistory 对象
history = ChatMessageHistory()
# 给 MessageHistory 对象添加对话内容
history.add_ai_message("你好!")
history.add_user_message("中国的首都是哪里?")
# 执行对话
ai_response = chat(history.messages)
print(ai_response)
使用 HuggingFace 模型
使用 HuggingFace 模型之前,需要先设置环境变量
import os
os.environ['HUGGINGFACEHUB_API_TOKEN'] = ''
使用在线的 HuggingFace 模型
from langchain import PromptTemplate, HuggingFaceHub, LLMChain
template = """Question: {question}
Answer: Let's think step by step."""
prompt = PromptTemplate(template=template, input_variables=["question"])
llm = HuggingFaceHub(repo_id="google/flan-t5-xl", model_kwargs={"temperature":0, "max_length":64})
llm_chain = LLMChain(prompt=prompt, llm=llm)
question = "What NFL team won the Super Bowl in the year Justin Beiber was born?"
print(llm_chain.run(question))
将 HuggingFace 模型直接拉到本地使用
from langchain.llms import HuggingFacePipeline
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline, AutoModelForSeq2SeqLM
model_id = 'google/flan-t5-large'
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForSeq2SeqLM.from_pretrained(model_id, load_in_8bit=True)
pipe = pipeline(
"text2text-generation",
model=model,
tokenizer=tokenizer,
max_length=100
)
local_llm = HuggingFacePipeline(pipeline=pipe)
print(local_llm('What is the capital of France? '))
llm_chain = LLMChain(prompt=prompt, llm=local_llm)
question = "What is the capital of England?"
print(llm_chain.run(question))
将模型拉到本地使用的好处:
训练模型 可以使用本地的 GPU 有些模型无法在 HuggingFace 运行
总结
所有的案例都基本已经结束了,希望大家能通过这篇文章的学习有所收获。这篇文章只是对 LangChain 一个初级的讲解,高级的功能希望大家继续探索。
并且因为 LangChain 迭代极快,所以后面肯定会随着AI继续的发展,还会迭代出更好用的功能,所以我非常看好这个开源库。
希望大家能结合 LangChain 开发出更有创意的产品,而不仅仅只搞一堆各种一键搭建chatgpt聊天客户端的那种产品。
01
本文章的所有范例代码都在这里,祝大家学习愉快。
https://colab.research.google.com/drive/1ArRVMiS-YkhUlobHrU6BeS8fF57UeaPQ?usp=sharing