赞
踩
在大语言模型 (LLMs) 的应用中,我们面临众多挑战,包括领域知识的缺乏、信息的准确性问题以及生成的虚假内容。检索增强生成 (RAG) 通过引入外部知识库等额外信息源,为这些问题提供了有效的缓解策略。RAG 在那些需要不断更新知识的知识密集型场景或特定领域应用中尤为有效。与其他方法相比,RAG 的一大优势是无需针对特定任务重新训练大语言模型。RAG 系统的核心组成部分,包括检索、生成和增强三大环节。
检索增强生成(Retrieval Augmented Generation),简称 RAG。是通过自有垂域数据库检索相关信息,然后合并成为提示模板,给大模型生成漂亮的回答。RAG是一个将输入与一组相关的支持文档结合起来的技术,这些文档通常来自于像维基百科这样的来源。这些文档被添加到输入提示中,一起送入文本生成器,从而产生最终的输出。RAG的这一机制特别适用于需要应对信息不断更新的场景,因为大语言模型(LLM)所依赖的参数知识本质上是静态的。通过RAG,语言模型可以不经过重新训练而直接访问最新信息,以便生成可靠的、基于检索的输出。
RAG通过检索到的证据来提高LLM响应的准确性、可控性和相关性,这对于在快速变化的环境中解决问题尤其有价值,能有效减少错误信息生成和性能下降的问题。具体步骤如下:
初级 RAG 采用了一个传统过程,包括索引建立、文档检索和内容生成。简单来说,系统根据用户的输入查询相关文档,然后将这些文档和一个提示语结合起来,交给模型生成最终的回答。如果涉及到多轮对话,还可以将对话历史整合到提示语中。
高级 RAG 解决了初级 RAG 面临的问题,尤其是在提高检索质量方面,包括优化检索前、检索时和检索后的各个过程。在检索前的准备阶段,我们通过优化数据的索引建立来提高数据质量,包括改善数据的细节度、优化索引结构、添加元数据、改进对齐方式以及混合检索方法。
模块化 RAG,顾名思义,通过增强其功能模块来提升性能,例如加入相似性检索的搜索模块,以及在检索工具上进行精细调整。模块化 RAG 能够根据具体的任务需求,添加、替换或调整模块之间的工作流程,从而实现更高的多样性和灵活性。这种设计让模块化 RAG 不仅包括了朴素 RAG 和高级 RAG 这两种固定模式,还扩展了包括搜索、记忆、融合、路由、预测和任务适配等多种模块,以解决各种问题。随着 RAG 系统构建变得更加灵活,一系列优化技术相继被提出,用于进一步优化 RAG 流程,包括:
一句话总结:RAG(中文为检索增强生成) = 检索技术 + LLM 提示。例如,我们向 LLM 提问一个问题(answer),RAG 从各种数据源检索相关的信息,并将检索到的信息和问题(answer)注入到 LLM 提示中,LLM 最后给出答案。完整的RAG应用流程主要包含两个阶段:
数据准备一般是一个离线的过程,主要是将私域数据向量化后构建索引并存入数据库的过程。主要包括:数据提取、文本分割、向量化、数据入库等环节。
构建 RAG 应用
将LLM 接入 LangChain,LangChain 为基于 LLM 开发自定义应用提供了高效的开发框架,便于开发者迅速地激发 LLM 的强大能力,搭建 LLM 应用。LangChain 也同样支持多种大模型,内置了 OpenAI、LLAMA 等大模型的调用接口。但是,LangChain 并没有内置所有大模型,它通过允许用户自定义 LLM 类型,来提供强大的可扩展性。LangChain 提供了对于多种大模型的封装,基于 LangChain 的接口可以便捷地调用 ChatGPT 并将其集合在以 LangChain 为基础框架搭建的个人应用中。基于 LangChain 接口调用 ChatGPT 同样需要配置你的个人密钥。
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']
接下来你需要实例化一个 ChatOpenAI 类,可以在实例化时传入超参数来控制回答,例如 temperature
参数。
# 这里我们将参数temperature设置为0.0,从而减少生成答案的随机性。
# 如果你想要每次得到不一样的有新意的答案,可以尝试调整该参数。
llm = ChatOpenAI(temperature=0.0)
llm = ChatOpenAI(temperature=0, openai_api_key="YOUR_API_KEY")
output = llm.invoke("请你自我介绍一下自己!")
默认调用的是 ChatGPT-3.5 模型。另外,几种常用的超参数设置包括:
在我们开发大模型应用时,大多数情况下不会直接将用户的输入直接传递给 LLM。通常,他们会将用户输入添加到一个较大的文本中,称为提示模板
,该文本提供有关当前特定任务的附加上下文。 PromptTemplates 正是帮助解决这个问题!它们捆绑了从用户输入到完全格式化的提示的所有逻辑。这可以非常简单地开始 - 例如,生成上述字符串的提示就是:
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)
output = llm.invoke(messages)
OutputParsers 将语言模型的原始输出转换为可以在下游使用的格式。 OutputParsers 有几种主要类型,包括:
最后,我们将模型输出传递给 output_parser
,它是一个 BaseOutputParser
,这意味着它接受字符串或 BaseMessage 作为输入。 StrOutputParser 特别简单地将任何输入转换为字符串。通过输出解析器成功将 ChatMessage
类型的输出解析为了字符串
。
from langchain_core.output_parsers import StrOutputParser
output_parser = StrOutputParser()
output_parser.invoke(output)
现在可以将所有这些组合成一条链。该链将获取输入变量,将这些变量传递给提示模板以创建提示,将提示传递给语言模型,然后通过(可选)输出解析器传递输出。接下来我们将使用LCEL这种语法去快速实现一条链(chain)。
chain = chat_prompt | llm | output_parser
chain.invoke({"input_language":"中文", "output_language":"英文","text": text})
构建检索问答链
首先,我们加载已经构建的向量数据库。注意,此处你需要使用和构建时相同的 Emedding。从环境变量中加载你的 API_KEY
import sys
sys.path.append("../C3 搭建知识库") # 将父目录放入系统路径中
# 使用智谱 Embedding API,注意,需要将上一章实现的封装代码下载到本地
from zhipuai_embedding import ZhipuAIEmbeddings
from langchain.vectorstores.chroma import Chroma
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")
调用 OpenAI 的 API 创建一个 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() 有如下参数:
基于召回结果和 query 结合起来构建的 prompt 效果
question_1 = "什么是南瓜书?"
question_2 = "王阳明是谁?"
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)
通过以上两个问题,我们发现 LLM 对于一些近几年的知识以及非常识性的专业问题,回答的并不是很好。而加上我们的本地知识,就可以帮助 LLM 做出更好的回答。另外,也有助于缓解大模型的“幻觉”问题。
现在我们已经实现了通过上传本地知识文档,然后将他们保存到向量知识库,通过将查询问题与向量知识库的召回结果进行结合输入到 LLM 中,我们就得到了一个相比于直接让 LLM 回答要好得多的结果。在与语言模型交互时,你可能已经注意到一个关键问题 - 它们并不记得你之前的交流内容。这在我们构建一些应用程序(如聊天机器人)的时候,带来了很大的挑战,使得对话似乎缺乏真正的连续性。将介绍 LangChain 中的储存模块,即如何将先前的对话嵌入到语言模型中的,使其具有连续对话的能力。我们将使用 ConversationBufferMemory
,它保存聊天消息历史记录的列表,这些历史记录将在回答问题时与问题一起传递给聊天机器人,从而将它们添加到上下文中。
from langchain.memory import ConversationBufferMemory
memory = ConversationBufferMemory(
memory_key="chat_history", # 与 prompt 的输入变量保持一致。
return_messages=True # 将以消息列表的形式返回聊天记录,而不是单个字符串
)
对话检索链(ConversationalRetrievalChain)在检索 QA 链的基础上,增加了处理对话历史的能力。它的工作流程是:
这种链式方式将新问题放在之前对话的语境中进行检索,可以处理依赖历史信息的查询。并保留所有信 息在对话记忆中,方便追踪。使用上一节中的向量数据库和 LLM !首先提出一个无历史对话的问题“我可以学习到关于提示工程的知识吗?”
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'])
对知识库和LLM已经有了基本的理解,现在是时候将它们巧妙地融合并打造成一个富有视觉效果的界面了。这样的界面不仅对操作更加便捷,还能便于与他人分享。Streamlit 是一种快速便捷的方法,可以直接在 Python 中通过友好的 Web 界面演示机器学习模型。Streamlit 可以通过 Python 接口程序快速实现这一目标,而无需编写任何前端、网页或 JavaScript 代码。
Streamlit
是一个用于快速创建数据应用程序的开源 Python 库。它的设计目标是让数据科学家能够轻松地将数据分析和机器学习模型转化为具有交互性的 Web 应用程序,而无需深入了解 Web 开发。和常规 Web 框架,如 Flask/Django 的不同之处在于,它不需要你去编写任何客户端代码(HTML/CSS/JS),只需要编写普通的 Python 模块,就可以在很短的时间内创建美观并具备高度交互性的界面,从而快速生成数据分析或者机器学习的结果;另一方面,和那些只能通过拖拽生成的工具也不同的是,你仍然具有对代码的完整控制权。Streamlit 提供了一组简单而强大的基础模块,用于构建数据应用程序:
这些基础模块使得通过 Streamlit 能够轻松地构建交互式数据应用程序,并且在使用时可以根据需要进行组合和定制,更多内容请查看官方文档
首先,创建一个新的 Python 文件并将其保存 streamlit_app.py在工作目录的根目录中,导入必要的 Python 库。
import streamlit as st
from langchain_openai import ChatOpenAI
st.title('动手学大模型应用开发')
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 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_app.py
要将应用程序部署到 Streamlit Cloud,请执行以下步骤:
New app
按钮,然后指定存储库、分支和主文件路径。或者,您可以通过选择自定义子域来自定义应用程序的 URLDeploy!
按钮,应用程序现在将部署到 Streamlit Community Cloud,并且可以从世界各地访问!Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。