一、简介
LangChian 作为一个大语言模型开发框架,是 LLM 应用架构的重要一环。那什么是 LLM 应用架构呢?其实就是指基于语言模型的应用程序设计和开发的架构。
LangChian 可以将 LLM 模型、向量数据库、交互层 Prompt、外部知识、外部工具整合到一起,进而可以自由构建 LLM 应用。
二、6大组件
LangChain 包含六部分组成,分别为:Models、Prompts、Indexes、Memory、Chains、Agents。
2.1 Models
LangChain本身不提供LLM,提供通用的接口访问LLM,可以很方便的更换底层的LLM以及自定义自己的LLM。主要有2大类的Models:
1)LLM:将文本字符串作为输入并返回文本字符串的模型,类似OpenAI的text-davinci-003
2)Chat Models:由语言模型支持但将聊天消息列表作为输入并返回聊天消息的模型。一般使用的ChatGPT以及Claude为Chat Models。
与模型交互的,基本上是通过给与Prompt的方式,LangChain通过PromptTemplate的方式方便我们构建以及复用Prompt。
2.1.1 LLM
LangChain支持各种大模型的使用,如
谷歌的 BERT
OpenAI 的 GPT-3
谷歌 LaMDA
谷歌 PaLM
Meta AI 的 LLaMA
OpenAI 的 GPT-4
借助 LangChain,与大语言模型的交互变得更加便捷。LangChain 提供的接口和功能有助于将 LLM 的强大能力轻松集成到你的工作应用程序中。LangChain 利用 asyncio 库为 LLM 提供异步支持。
对于需要同时并发调用多个 LLM 的网络绑定场景,LangChain 还提供了异步支持。通过释放处理请求的线程,服务器可以将其分配给其他任务,直到响应准备就绪,从而最大限度地提高资源利用率。
目前,LangChain 支持 OpenAI、PromptLayerOpenAI、ChatOpenAI 和 Anthropic 等模型的异步支持,但在未来的计划中将扩展对其他 LLM 的异步支持。你可以使用 agenerate 方法来异步调用 OpenAI LLM。此外,你还可以编写自定义的 LLM 包装器,而不仅限于 LangChain 所支持的模型。
2.1.2 Chat Models
LangChain 为使用聊天模型提供了一个标准接口。聊天模型是语言模型的一种变体。虽然聊天模型在内部使用语言模型,但它们所提供的接口略有不同。它们不是暴露一个 “输入文本,输出文本” 的 API,而是提供了一个以 “聊天消息” 作为输入和输出的接口。
聊天模型的接口是基于消息而不是原始文本。LangChain 目前支持的消息类型有 AIMessage、HumanMessage、SystemMessage 和 ChatMessage,其中 ChatMessage 接受一个任意的角色参数。大多数情况下,您只需要处理 HumanMessage、AIMessage 和 SystemMessage。
# 导入OpenAI的聊天模型,及消息类型
from langchain.chat_models import ChatOpenAI
from langchain.schema import (
AIMessage,
HumanMessage,
SystemMessage
)
# 初始化聊天对象
chat = ChatOpenAI(openai_api_key="")
# 向聊天模型发问
chat([HumanMessage(content="Translate this sentence from English to French: I love programming.")])
OpenAI 聊天模式支持多个消息作为输入。这是一个系统和用户消息聊天模式的例子:
messages = [
SystemMessage(content="You are a helpful assistant that translates English to French."),
HumanMessage(content="I love programming.")
]
chat(messages)
批量处理,批量输出
batch_messages = [
[
SystemMessage(content="You are a helpful assistant that translates English to French."),
HumanMessage(content="I love programming.")
],
[
SystemMessage(content="You are a helpful assistant that translates English to French."),
HumanMessage(content="I love artificial intelligence.")
],
]
result = chat.generate(batch_messages)
result
LangChain 也很贴心的提供了缓存的功能。并且提供了两种缓存方案,内存缓存方案和数据库缓存方案,当然支持的数据库缓存方案有很多种。
# 导入聊天模型,SQLiteCache模块
import os
os.environ["OPENAI_API_KEY"] = 'your apikey'
import langchain
from langchain.chat_models import ChatOpenAI
from langchain.cache import SQLiteCache
# 设置语言模型的缓存数据存储的地址
langchain.llm_cache = SQLiteCache(database_path=".langchain.db")
# 加载 llm 模型
llm = ChatOpenAI()
# 第一次向模型提问
result = llm.predict('tell me a joke')
print(result)
# 第二次向模型提问同样的问题
result2 = llm.predict('tell me a joke')
print(result2)
2.2 Prompts(提示)
提示(prompt)是我们向系统提供的输入,以便根据我们的使用案例对答案进行精确或特定的调整。许多时候,我们希望得到的不仅仅是文本,还需要更结构化的信息。基于对比预训练和零样本学习的许多新的目标检测和分类算法都将提示作为有效的输入来进行结果预测。举例来说,OpenAI 的 CLIP 和 META 的 Grounding DINO 都使用提示作为预测的输入。
在 LangChain 中,我们可以根据需要设置提示模板,并将其与主链相连接以进行输出预测。此外,LangChain 还提供了输出解析器的功能,用于进一步精炼结果。输出解析器的作用是指导模型输出的格式化方式,和将输出解析为所需的格式。
在 LangChain 中,我们可以提供提示模板作为输入。模板指的是我们希望获得答案的具体格式或蓝图。LangChain 提供了预先设计好的提示模板,可以用于生成不同类型任务的提示。然而,在某些情况下,预设的模板可能无法满足你的需求。在这种情况下,我们可以使用自定义的提示模板。
from langchain.llms import OpenAI
# 示例
def generate_text(features):
prompt_template = "我想让你帮忙生成10条关于{}的文案"
prompt = prompt_template.format(features)
llm = OpenAI()
response = llm.generate(prompt, max_tokens=100, temperature=0.8)
text = [gen[0].text.strip() for gen in response.generations]
return text
features = "星空"
text = generate_text(features)
print(text)
2.3 Chains(链)
链允许我们将多个组件组合在一起以创建一个单一的、连贯的任务。例如,我们可以创建一个链,它接受用户输入,使用 PromptTemplate 对其进行格式化,然后将格式化的响应传递给 LLM。另外我们也可以通过将多个链组合在一起,或者将链与其他组件组合来构建更复杂的链。
2.3.1. LLMChain
LLMChain 是一个简单的链,它围绕语言模型添加了一些功能。它在整个 LangChain 中广泛使用,包括在其他链和代理中。它接受一个提示模板,将其与用户输入进行格式化,并返回 LLM 的响应。
from langchain import PromptTemplate, OpenAI, LLMChain
prompt_template = "What is a good name for a company that makes {product}?"
llm = OpenAI(temperature=0)
llm_chain = LLMChain(
llm=llm,
prompt=PromptTemplate.from_template(prompt_template)
)
llm_chain("colorful socks")
2.3.2. SimpleSequentialChain
顺序链的最简单形式,其中每个步骤都有一个单一的输入/输出,并且一个步骤的输出是下一步的输入。
from langchain.llms import OpenAI
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain.chains import SimpleSequentialChain
# 定义第一个chain
llm = OpenAI(temperature=.7)
template = """You are a playwright. Given the title of play, it is your job to write a synopsis for that title.
Title: {title}
Playwright: This is a synopsis for the above play:"""
prompt_template = PromptTemplate(input_variables=["title"], template=template)
synopsis_chain = LLMChain(llm=llm, prompt=prompt_template)
# 定义第二个chain
llm = OpenAI(temperature=.7)
template = """You are a play critic from the New York Times. Given the synopsis of play, it is your job to write a review for that play.
Play Synopsis:
{synopsis}
Review from a New York Times play critic of the above play:"""
prompt_template = PromptTemplate(input_variables=["synopsis"], template=template)
review_chain = LLMChain(llm=llm, prompt=prompt_template)
# 通过简单顺序链组合两个LLMChain
overall_chain = SimpleSequentialChain(chains=[synopsis_chain, review_chain], verbose=True)
# 执行顺序链
review = overall_chain.run("Tragedy at sunset on the beach")
2.3.3. SequentialChain
相比 SimpleSequentialChain 只允许有单个输入输出,它是一种更通用的顺序链形式,允许多个输入/输出。
特别重要的是: 我们如何命名输入/输出变量名称。在上面的示例中,我们不必考虑这一点,因为我们只是将一个链的输出直接作为输入传递给下一个链,但在这里我们确实需要担心这一点,因为我们有多个输入。
# 这是一个 LLMChain,根据戏剧的标题和设定的时代,生成一个简介。
llm = OpenAI(temperature=.7)
template = """You are a playwright. Given the title of play and the era it is set in, it is your job to write a synopsis for that title.
# 这里定义了两个输入变量title和era,并定义一个输出变量:synopsis
Title: {title}
Era: {era}
Playwright: This is a synopsis for the above play:"""
prompt_template = PromptTemplate(input_variables=["title", "era"], template=template)
synopsis_chain = LLMChain(llm=llm, prompt=prompt_template, output_key="synopsis")
# 这是一个 LLMChain,根据剧情简介撰写一篇戏剧评论。
llm = OpenAI(temperature=.7)
template = """You are a play critic from the New York Times. Given the synopsis of play, it is your job to write a review for that play.
# 定义了一个输入变量:synopsis,输出变量:review
Play Synopsis:
{synopsis}
Review from a New York Times play critic of the above play:"""
prompt_template = PromptTemplate(input_variables=["synopsis"], template=template)
review_chain = LLMChain(llm=llm, prompt=prompt_template, output_key="review")
overall_chain({"title":"Tragedy at sunset on the beach", "era": "Victorian England"})
2.4. Indexes(索引)
索引是指对文档进行结构化的方法,以便 LLM 能够更好的与之交互。该组件主要包括:Document Loaders(文档加载器)、Text Splitters(文本拆分器)、VectorStores(向量存储器)以及 Retrievers(检索器)。
2.4.1. Document Loaders
指定源进行加载数据的。将特定格式的数据,转换为文本。如 CSV、File Directory、HTML、
JSON、Markdown、PDF。另外使用相关接口处理本地知识,或者在线知识。如 AirbyteJSON
Airtable、Alibaba Cloud MaxCompute、wikipedia、BiliBili、GitHub、GitBook 等等。
2.4.2. Text Splitters
由于模型对输入的字符长度有限制,我们在碰到很长的文本时,需要把文本分割成多个小的文本片段。
文本分割最简单的方式是按照字符长度进行分割,但是这会带来很多问题,比如说如果文本是一段代码,一个函数被分割到两段之后就成了没有意义的字符,所以整体的原则是把语义相关的文本片段放在一起。
LangChain 中最基本的文本分割器是 CharacterTextSplitter ,它按照指定的分隔符(默认“\n\n”)进行分割,并且考虑文本片段的最大长度。
from langchain.text_splitter import CharacterTextSplitter
# 初始字符串
state_of_the_union = "..."
text_splitter = CharacterTextSplitter(
separator = "\\n\\n",
chunk_size = 1000,
chunk_overlap = 200,
length_function = len,
)
texts = text_splitter.create_documents([state_of_the_union])
LangChain 还支持多个高级文本分割器
2.4.3. VectorStores
存储提取的文本向量,包括 Faiss、Milvus、Pinecone、Chroma 等。如下是 LangChain 集成的向量数据库。
2.4.4. Retrievers
检索器是一种便于模型查询的存储数据的方式,LangChain 约定检索器组件至少有一个方法 get_relevant_texts,这个方法接收查询字符串,返回一组文档。
from langchain.chains import RetrievalQA
from langchain.llms import OpenAI
from langchain.document_loaders import TextLoader
from langchain.indexes import VectorstoreIndexCreator
loader = TextLoader('../state_of_the_union.txt', encoding='utf8')
# 对加载的内容进行索引
index = VectorstoreIndexCreator().from_loaders([loader])
query = "What did the president say about Ketanji Brown Jackson"
# 通过query的方式找到语义检索的结果
index.query(query)
2.5 Memory(记忆)
一般在与 LLM 的交互过程中,模型是无法记住之前对话的历史消息的,无法实现跨越上下文的流畅对话应用。ChatGPT 提供了短期的 Memory(记忆),在每个交互 session 的问答里 ChatGPT 都能记住这个对话的上文(通过每次请求时把之前的问答 token 传给 OpenAI 来实现),但在新的交互 session 里 ChatGPT 就没有之前 session 的记忆。 LangChain 提供了多种不同的记忆形式,开发者可以选择存储完整记忆、仅保留最后几轮对话记忆或是限制存储的 token 数等。除此之外,开发者也可以选择将对话历史存储在向量数据库中,或是将某些特定实体的信息记忆起来。
第一次发送:
import openai
openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello"},
]
)
第二次发送就要带上我们第一次的记录:
import openai
openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hello, how can I help you?"},
{"role": "user", "content": "who is more stylish Pikachu or Neo"},
]
)
langchain 提供了不同的 Memory 组件完成内容记忆,如下是目前提供的组件。
2.5.1. ConversationBufferMemory
该组件类似我们上面的描述,只不过它会将聊天内容记录在内存中,而不需要每次再手动拼接聊天记录。
2.5.2. ConversationBufferWindowMemory
相比较第一个记忆组件,该组件增加了一个窗口参数,会保存最近K轮的聊天内容。
2.5.3. ConversationTokenBufferMemory
在内存中保留最近交互的缓冲区,并使用 token 长度而不是交互次数来确定何时刷新交互。
2.5.4. ConversationSummaryMemory
相比第一个记忆组件,该组件只会存储一个用户和机器人之间的聊天内容的摘要。
2.5.5. ConversationSummaryBufferMemory
结合了上面两个思路,存储一个用户和机器人之间的聊天内容的摘要并使用 token 长度来确定何时刷新交互。
2.5.6. VectorStoreRetrieverMemory
它是将所有之前的对话通过向量的方式存储到 VectorDB(向量数据库)中,在每一轮新的对话中,会根据用户的输入信息,匹配向量数据库中最相似的 K 组对话。
2.6. Agents(代理)
一些应用程序需要根据用户输入灵活地调用 LLM 和其他工具的链。代理接口为这样的应用程序提供了灵活性。代理可以访问一套工具,并根据用户输入确定要使用哪些工具。我们可以简单的理解为他可以动态的帮我们选择和调用 chain 或者已有的工具。代理主要有两种类型 Action agents 和 Plan-and-execute agents。
2.6.1. Action agents
行为代理: 在每个时间步,使用所有先前动作的输出来决定下一个动作。下图展示了行为代理执行的流程。
2.6.2. Plan-and-execute agents
预先决定完整的操作顺序,然后执行所有操作而不更新计划,下面是其流程。
● 接收用户输入
● 计划要采取的完整步骤顺序
● 按顺序执行步骤,将过去步骤的输出作为未来步骤的输入传递
三、构建本地知识库问答机器人
导入os,设置环境变量。导入OpenAI嵌入模型、Chroma向量数据库、文本分割器、OpenAI模型、向量数据库数据查询模块及文件夹文档加载器
import os
os.environ["OPENAI_API_KEY"] = '你的api key'
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
# 获取当前脚本所在的目录
base_dir = os.path.dirname(os.path.abspath(__file__))
# 构建doc.txt文件的路径
doc_Directory = os.path.join(base_dir, 'static')
# 加载文件夹中的所有txt类型的文件
loader = DirectoryLoader(doc_Directory, 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()
# 持久化数据
docsearch = Chroma.from_documents(split_docs, embeddings, persist_directory="D:/vector_store")
docsearch.persist()
# 从已有文件中加载数据
docsearch = Chroma(persist_directory="D:/vector_store", embedding_function=embeddings)
# 创建问答对象
qa = VectorDBQA.from_chain_type(llm=OpenAI(), chain_type="stuff", vectorstore=docsearch,return_source_documents=True)
# 进行问答
result = qa({"query": "中国面积多大?"})