CH4 搭建RAG应用 - SunXiaoXiang/llm-universe GitHub Wiki
LLM接入LangChain
基于LangChain调用ChatGPT
Models 模型
从 langchain.chat_models 导入 OpenAI 的对话模型 ChatOpenAI 。 除去OpenAI以外,langchain.chat_models 还集成了其他对话模型。
import os
import openai
from dotenv import load_dotenv, find_dotenv
# 读取本地/项目的环境变量。
# find_dotenv()寻找并定位.env文件的路径
# load_dotenv()读取该.env文件,并将其中的环境变量加载到当前的运行环境中
# 如果你设置的是全局的环境变量,这行代码则没有任何作用。
_ = load_dotenv(find_dotenv())
# 获取环境变量 OPENAI_API_KEY
openai_api_key = os.environ['OPENAI_API_KEY']
from langchain_openai import ChatOpenAI
# 这里我们将参数temperature设置为0.0,从而减少生成答案的随机性。
# 如果你想要每次得到不一样的有新意的答案,可以尝试调整该参数。
llm = ChatOpenAI(temperature=0.0)
llm
#手动指定API密钥,请使用以下代码:
#llm = ChatOpenAI(temperature=0, openai_api_key="YOUR_API_KEY")
默认调用的是 ChatGPT-3.5 模型。另外,几种常用的超参数设置包括:
· model_name:所要使用的模型,默认为 ‘gpt-3.5-turbo’,参数设置与 OpenAI 原生接口参数设置一致。
· temperature:温度系数,取值同原生接口。
· openai_api_key:OpenAI API key,如果不使用环境变量设置 API Key,也可以在实例化时设置。
· openai_proxy:设置代理,如果不使用环境变量设置代理,也可以在实例化时设置。
· streaming:是否使用流式传输,即逐字输出模型回答,默认为 False,此处不赘述。
· max_tokens:模型输出的最大 token 数,意义及取值同上。
当我们初始化了你选择的LLM
后,我们就可以尝试使用它!让我们问一下“请你自我介绍一下自己!”
output = llm.invoke("请你自我介绍一下自己!")
Prompt(提示模版)
在我们开发大模型应用时,大多数情况下不会直接将用户的输入直接传递给 LLM。通常,他们会将用户输入添加到一个较大的文本中,称为提示模板
,该文本提供有关当前特定任务的附加上下文。 PromptTemplates 正是帮助解决这个问题!它们捆绑了从用户输入到完全格式化的提示的所有逻辑。这可以非常简单地开始 - 例如,生成上述字符串的提示就是:
我们需要先构造一个个性化 Template:
from langchain_core.prompts import ChatPromptTemplate
# 这里我们要求模型对给定文本进行中文翻译
prompt = """请你将由三个反引号分割的文本翻译成英文!\
text: ```{text}```
"""
一个构造好的完整的提示模版示例子
text = "我带着比身体重的行李,\
游入尼罗河底,\
经过几道闪电 看到一堆光圈,\
不确定是不是这里。\
"
prompt.format(text=text)
聊天模型的接口是基于消息(message),而不是原始的文本。PromptTemplates 也可以用于产生消息列表,在这种样例中,prompt
不仅包含了输入内容信息,也包含了每条message
的信息(角色、在列表中的位置等)。通常情况下,一个 ChatPromptTemplate
是一个 ChatMessageTemplate
的列表。每个 ChatMessageTemplate
包含格式化该聊天消息的说明(其角色以及内容)。
让我们一起看一个示例:
from langchain.prompts.chat import ChatPromptTemplate
template = "你是一个翻译助手,可以帮助我将 {input_language} 翻译成 {output_language}."
human_template = "{text}"
chat_prompt = ChatPromptTemplate.from_messages([
("system", template),
("human", human_template),
])
text = "我带着比身体重的行李,\
游入尼罗河底,\
经过几道闪电 看到一堆光圈,\
不确定是不是这里。\
"
messages = chat_prompt.format_messages(input_language="中文", output_language="英文", text=text)
messages
调用定义好的llm
和messages
来输出回答:
output = llm.invoke(messages)
output
output parser(输出解析器)
OutputParsers 将语言模型的原始输出转换为可以在下游使用的格式。 OutputParsers 有几种主要类型,包括:
- 将 LLM 文本转换为结构化信息(例如 JSON)
- 将 ChatMessage 转换为字符串
- 将除消息之外的调用返回的额外信息(如 OpenAI 函数调用)转换为字符串
最后,我们将模型输出传递给 output_parser
,它是一个 BaseOutputParser
,这意味着它接受字符串或 BaseMessage 作为输入。 StrOutputParser 特别简单地将任何输入转换为字符串。
from langchain_core.output_parsers import StrOutputParser
output_parser = StrOutputParser()
output_parser.invoke(output)
从上面结果可以看到,我们通过输出解析器成功将 ChatMessage
类型的输出解析为了字符串
完整流程
chain = prompt | model | output_parser
什么是 LCEL ? LCEL(LangChain Expression Language,Langchain的表达式语言),LCEL是一种新的语法,是LangChain工具包的重要补充,他有许多优点,使得我们处理LangChain和代理更加简单方便。
- LCEL提供了异步、批处理和流处理支持,使代码可以快速在不同服务器中移植。
- LCEL拥有后备措施,解决LLM格式输出的问题。
- LCEL增加了LLM的并行性,提高了效率。
- LCEL内置了日志记录,即使代理变得复杂,有助于理解复杂链条和代理的运行情况
我们现在可以将所有这些组合成一条链。该链将获取输入变量,将这些变量传递给提示模板以创建提示,将提示传递给语言模型,然后通过(可选)输出解析器传递输出。接下来我们将使用LCEL这种语法去快速实现一条链(chain)。让我们看看它的实际效果!
chain = chat_prompt | llm | output_parser
chain.invoke({"input_language":"中文", "output_language":"英文","text": text})
基于LangChain调用AzureOpenAI
Models 模型
import os
import openai
from dotenv import load_dotenv, find_dotenv
from langchain_openai import AzureOpenAI
from langchain_openai import AzureChatOpenAI
# 读取本地/项目的环境变量。
# find_dotenv()寻找并定位.env文件的路径
# load_dotenv()读取该.env文件,并将其中的环境变量加载到当前的运行环境中
# 如果你设置的是全局的环境变量,这行代码则没有任何作用。
_ = load_dotenv(find_dotenv())
# 获取环境变量
openai_api_type = "azure"
AZURE_OPENAI_API_KEY = os.environ['AZURE_OPENAI_API_KEY']
AZURE_OPENAI_DEPLOYMENT_ID = os.environ['AZURE_OPENAI_DEPLOYMENT_ID']
AZURE_OPENAI_ENDPOINT = os.environ['AZURE_OPENAI_ENDPOINT']
API_VERSION = os.environ['API_VERSION']
llm = AzureChatOpenAI(
azure_endpoint=AZURE_OPENAI_ENDPOINT,
api_key=AZURE_OPENAI_API_KEY,
azure_deployment=AZURE_OPENAI_DEPLOYMENT_ID,
api_version=API_VERSION,
temperature=0.0
)
output = llm.invoke("介绍一下你自己")
print(output)
构建检索问答链
加载向量数据库
首先,我们加载在前一章已经构建的向量数据库。注意,此处你需要使用和构建时相同的 Emedding。
import sys
sys.path.append("../C3 搭建知识库") # 将父目录放入系统路径中
# 使用智谱 Embedding API,注意,需要将上一章实现的封装代码下载到本地
from zhipuai_embedding import ZhipuAIEmbeddings
from langchain.vectorstores.chroma import Chroma
加载你自己的key
from dotenv import load_dotenv, find_dotenv
import os
_ = load_dotenv(find_dotenv()) # read local .env file
zhipuai_api_key = os.environ['ZHIPUAI_API_KEY']
加载向量数据库,其中包含了 ../../data_base/knowledge_db 下多个文档的 Embedding
# 定义 Embeddings
embedding = ZhipuAIEmbeddings()
# 向量数据库持久化路径
persist_directory = '../C3 搭建知识库/data_base/vector_db/chroma'
# 加载数据库
vectordb = Chroma(
persist_directory=persist_directory, # 允许我们将persist_directory目录保存到磁盘上
embedding_function=embedding
)
print(f"向量库中存储的数量:{vectordb._collection.count()}")
我们可以测试一下加载的向量数据库,使用一个问题 query 进行向量检索。如下代码会在向量数据库中根据相似性进行检索,返回前 k 个最相似的文档。
⚠️使用相似性搜索前,请确保你已安装了 OpenAI 开源的快速分词工具 tiktoken 包:
pip install tiktoken
question = "什么是prompt engineering?"
docs = vectordb.similarity_search(question,k=3)
print(f"检索到的内容数:{len(docs)}")
打印下检索的问题
for i, doc in enumerate(docs):
print(f"检索到的第{i}个内容: \n {doc.page_content}", end="\n-----------------------------------------------------\n")
创建一个LLM
import os
OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model_name = "gpt-3.5-turbo", temperature = 0)
llm.invoke("请你自我介绍一下自己!")
创建检索问答链
from langchain.prompts import PromptTemplate
template = """使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答
案。最多使用三句话。尽量使答案简明扼要。总是在回答的最后说“谢谢你的提问!”。
{context}
问题: {question}
"""
QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context","question"],
template=template)
再创建一个基于模板的检索链:
from langchain.chains import RetrievalQA
qa_chain = RetrievalQA.from_chain_type(llm,
retriever=vectordb.as_retriever(),
return_source_documents=True,
chain_type_kwargs={"prompt":QA_CHAIN_PROMPT})
创建检索 QA 链的方法 RetrievalQA.from_chain_type() 有如下参数:
- llm:指定使用的 LLM
- 指定 chain type : RetrievalQA.from_chain_type(chain_type="map_reduce"),也可以利用load_qa_chain()方法指定chain type。
- 自定义 prompt :通过在RetrievalQA.from_chain_type()方法中,指定chain_type_kwargs参数,而该参数:chain_type_kwargs = {"prompt": PROMPT}
- 返回源文档:通过RetrievalQA.from_chain_type()方法中指定:return_source_documents=True参数;也可以使用RetrievalQAWithSourceChain()方法,返回源文档的引用(坐标或者叫主键、索引)
检索问答链效果测试
question_1 = "什么是南瓜书?"
question_2 = "王阳明是谁?"
基于召回结果和query结合起来构建的prompt效果
result = qa_chain({"query": question_1})
print("大模型+知识库后回答 question_1 的结果:")
print(result["result"])
result = qa_chain({"query": question_2})
print("大模型+知识库后回答 question_2 的结果:")
print(result["result"])
大模型自己回答的效果
prompt_template = """请回答下列问题:
{}""".format(question_1)
### 基于大模型的问答
llm.predict(prompt_template)
prompt_template = """请回答下列问题:
{}""".format(question_2)
### 基于大模型的问答
llm.predict(prompt_template)
添加历史对话的记忆功能
记忆(Memory)
在本节中我们将介绍 LangChain 中的储存模块,即如何将先前的对话嵌入到语言模型中的,使其具有连续对话的能力。我们将使用 ConversationBufferMemory
,它保存聊天消息历史记录的列表,这些历史记录将在回答问题时与问题一起传递给聊天机器人,从而将它们添加到上下文中。
from langchain.memory import ConversationBufferMemory
memory = ConversationBufferMemory(
memory_key="chat_history", # 与 prompt 的输入变量保持一致。
return_messages=True # 将以消息列表的形式返回聊天记录,而不是单个字符串
)
对话检索链(ConversationRetrievalChain)
对话检索链(ConversationalRetrievalChain)在检索 QA 链的基础上,增加了处理对话历史的能力。
它的工作流程是:
- 将之前的对话与新问题合并生成一个完整的查询语句。
- 在向量数据库中搜索该查询的相关文档。
- 获取结果后,存储所有答案到对话记忆区。
- 用户可在 UI 中查看完整的对话流程。
这种链式方式将新问题放在之前对话的语境中进行检索,可以处理依赖历史信息的查询。并保留所有信 息在对话记忆中,方便追踪。 接下来让我们可以测试这个对话检索链的效果: 使用上一节中的向量数据库和 LLM !首先提出一个无历史对话的问题“这门课会学习 Python 吗?”,并查看回答。
from langchain.chains import ConversationalRetrievalChain
retriever=vectordb.as_retriever()
qa = ConversationalRetrievalChain.from_llm(
llm,
retriever=retriever,
memory=memory
)
question = "我可以学习到关于提示工程的知识吗?"
result = qa({"question": question})
print(result['answer'])
question = "为什么这门课需要教这方面的知识?"
result = qa({"question": question})
print(result['answer'])
部署知识库助手
streamlit介绍
是一个用于快速创建数据应用程序的开源 Python 库。它的设计目标是让数据科学家能够轻松地将数据分析和机器学习模型转化为具有交互性的 Web 应用程序,而无需深入了解 Web 开发。和常规 Web 框架,如 Flask/Django 的不同之处在于,它不需要你去编写任何客户端代码(HTML/CSS/JS),只需要编写普通的 Python 模块,就可以在很短的时间内创建美观并具备高度交互性的界面,从而快速生成数据分析或者机器学习的结果;另一方面,和那些只能通过拖拽生成的工具也不同的是,你仍然具有对代码的完整控制权。
Streamlit 提供了一组简单而强大的基础模块,用于构建数据应用程序:
-
st.write():这是最基本的模块之一,用于在应用程序中呈现文本、图像、表格等内容。
-
st.title()、st.header()、st.subheader():这些模块用于添加标题、子标题和分组标题,以组织应用程序的布局。
-
st.text()、st.markdown():用于添加文本内容,支持 Markdown 语法。
-
st.image():用于添加图像到应用程序中。
-
st.dataframe():用于呈现 Pandas 数据框。
-
st.table():用于呈现简单的数据表格。
-
st.pyplot()、st.altair_chart()、st.plotly_chart():用于呈现 Matplotlib、Altair 或 Plotly 绘制的图表。
-
st.selectbox()、st.multiselect()、st.slider()、st.text_input():用于添加交互式小部件,允许用户在应用程序中进行选择、输入或滑动操作。
-
st.button()、st.checkbox()、st.radio():用于添加按钮、复选框和单选按钮,以触发特定的操作。
这些基础模块使得通过 Streamlit 能够轻松地构建交互式数据应用程序,并且在使用时可以根据需要进行组合和定制,更多内容请查看官方文档
构建应用程序
import streamlit as st
from langchain_openai import ChatOpenAI
首先,创建一个新的 Python 文件并将其保存 streamlit_app.py在工作目录的根目录中
- 导入必要的 Python 库。
import streamlit as st
from langchain_openai import ChatOpenAI
- 创建应用程序的标题
st.title
st.title('🦜🔗 动手学大模型应用开发')
- 添加一个文本输入框,供用户输入其 OpenAI API 密钥
openai_api_key = st.sidebar.text_input('OpenAI API Key', type='password')
- 定义一个函数,使用用户密钥对 OpenAI API 进行身份验证、发送提示并获取 AI 生成的响应。该函数接受用户的提示作为参数,并使用
st.info
来在蓝色框中显示 AI 生成的响应
def generate_response(input_text):
llm = ChatOpenAI(temperature=0.7, openai_api_key=openai_api_key)
st.info(llm(input_text))
- 最后,使用
st.form()
创建一个文本框(st.text_area())供用户输入。当用户单击Submit
时,generate-response()
将使用用户的输入作为参数来调用该函数
with st.form('my_form'):
text = st.text_area('Enter text:', 'What are the three key pieces of advice for learning how to code?')
submitted = st.form_submit_button('Submit')
if not openai_api_key.startswith('sk-'):
st.warning('Please enter your OpenAI API key!', icon='⚠')
if submitted and openai_api_key.startswith('sk-'):
generate_response(text)
-
保存当前的文件
streamlit_app.py
! -
返回计算机的终端以运行该应用程序
streamlit run streamlit_app.py
结果展示如下:
但是当前只能进行单轮对话,我们对上述做些修改,通过使用 st.session_state
来存储对话历史,可以在用户与应用程序交互时保留整个对话的上下文。
具体代码如下:
# Streamlit 应用程序界面
def main():
st.title('🦜🔗 动手学大模型应用开发')
openai_api_key = st.sidebar.text_input('OpenAI API Key', type='password')
# 用于跟踪对话历史
if 'messages' not in st.session_state:
st.session_state.messages = []
messages = st.container(height=300)
if prompt := st.chat_input("Say something"):
# 将用户输入添加到对话历史中
st.session_state.messages.append({"role": "user", "text": prompt})
# 调用 respond 函数获取回答
answer = generate_response(prompt, openai_api_key)
# 检查回答是否为 None
if answer is not None:
# 将LLM的回答添加到对话历史中
st.session_state.messages.append({"role": "assistant", "text": answer})
# 显示整个对话历史
for message in st.session_state.messages:
if message["role"] == "user":
messages.chat_message("user").write(message["text"])
elif message["role"] == "assistant":
messages.chat_message("assistant").write(message["text"])
添加检索问答
先将2.构建检索问答链
部分的代码进行封装:
- get_vectordb函数返回C3部分持久化后的向量知识库
- get_chat_qa_chain函数返回调用带有历史记录的检索问答链后的结果
- get_qa_chain函数返回调用不带有历史记录的检索问答链后的结果
def get_vectordb():
# 定义 Embeddings
embedding = ZhipuAIEmbeddings()
# 向量数据库持久化路径
persist_directory = '../C3 搭建知识库/data_base/vector_db/chroma'
# 加载数据库
vectordb = Chroma(
persist_directory=persist_directory, # 允许我们将persist_directory目录保存到磁盘上
embedding_function=embedding
)
return vectordb
#带有历史记录的问答链
def get_chat_qa_chain(question:str,openai_api_key:str):
vectordb = get_vectordb()
llm = ChatOpenAI(model_name = "gpt-3.5-turbo", temperature = 0,openai_api_key = openai_api_key)
memory = ConversationBufferMemory(
memory_key="chat_history", # 与 prompt 的输入变量保持一致。
return_messages=True # 将以消息列表的形式返回聊天记录,而不是单个字符串
)
retriever=vectordb.as_retriever()
qa = ConversationalRetrievalChain.from_llm(
llm,
retriever=retriever,
memory=memory
)
result = qa({"question": question})
return result['answer']
#不带历史记录的问答链
def get_qa_chain(question:str,openai_api_key:str):
vectordb = get_vectordb()
llm = ChatOpenAI(model_name = "gpt-3.5-turbo", temperature = 0,openai_api_key = openai_api_key)
template = """使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答
案。最多使用三句话。尽量使答案简明扼要。总是在回答的最后说“谢谢你的提问!”。
{context}
问题: {question}
"""
QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context","question"],
template=template)
qa_chain = RetrievalQA.from_chain_type(llm,
retriever=vectordb.as_retriever(),
return_source_documents=True,
chain_type_kwargs={"prompt":QA_CHAIN_PROMPT})
result = qa_chain({"query": question})
return result["result"]
然后,添加一个单选按钮部件st.radio
,选择进行问答的模式:
- None:不使用检索问答的普通模式
- qa_chain:不带历史记录的检索问答模式
- chat_qa_chain:带历史记录的检索问答模式
selected_method = st.radio(
"你想选择哪种模式进行对话?",
["None", "qa_chain", "chat_qa_chain"],
captions = ["不使用检索问答的普通模式", "不带历史记录的检索问答模式", "带历史记录的检索问答模式"])
最终的效果如下:
进入页面,首先先输入OPEN_API_KEY(默认),然后点击单选按钮选择进行问答的模式,最后在输入框输入你的问题,按下回车即可!
部署应用
要将应用程序部署到 Streamlit Cloud,请执行以下步骤:
-
为应用程序创建 GitHub 存储库。您的存储库应包含两个文件:
your-repository/
├── streamlit_app.py
└── requirements.txt -
转到 Streamlit Community Cloud,单击工作区中的
New app
按钮,然后指定存储库、分支和主文件路径。或者,您可以通过选择自定义子域来自定义应用程序的 URL -
点击
Deploy!
按钮
您的应用程序现在将部署到 Streamlit Community Cloud,并且可以从世界各地访问! 🌎
我们的项目部署到这基本完成,为了方便进行演示进行了简化,还有很多可以进一步优化的地方,期待学习者们进行各种魔改!
优化方向:
- 界面中添加上传本地文档,建立向量数据库的功能
- 添加多种LLM 与 embedding方法选择的按钮
- 添加修改参数的按钮
- 更多......