之前我已經(jīng)完成了使用langchain與你自己的數(shù)據(jù)對話的前四篇博客,還沒有閱讀這四篇博客的朋友可以先閱讀一下:
- 使用langchain與你自己的數(shù)據(jù)對話(一):文檔加載與切割
- 使用langchain與你自己的數(shù)據(jù)對話(二):向量存儲(chǔ)與嵌入
- 使用langchain與你自己的數(shù)據(jù)對話(三):檢索(Retrieval)
- 使用langchain與你自己的數(shù)據(jù)對話(四):問答(question answering)?
今天我們來繼續(xù)講解deepleaning.AI的在線課程“LangChain: Chat with Your Data”的第六門課:chat。
Langchain在實(shí)現(xiàn)與外部數(shù)據(jù)對話的功能時(shí)需要經(jīng)歷下面的5個(gè)階段,它們分別是:Document Loading->Splitting->Storage->Retrieval->Output,如下圖所示:
在前面的四篇博客中我們以及完成了這5給階段所有的內(nèi)容介紹,并在第四篇博客中我們還創(chuàng)建了RetrievalQA實(shí)現(xiàn)了對數(shù)據(jù)的問答功能,但是這里有一個(gè)小小的缺陷,那就是通過RetrievalQA實(shí)現(xiàn)的問答功能只能針對當(dāng)前問題進(jìn)行回答,它無法參考上下文來來回答問題,也就是說它沒有記憶能力,無法實(shí)現(xiàn)連貫性聊。今天我們就來解決這個(gè)問題,我們會(huì)創(chuàng)建一個(gè)真正的個(gè)性化聊天機(jī)器人,它會(huì)學(xué)習(xí)用戶提供的數(shù)據(jù),并解答任何關(guān)于數(shù)據(jù)內(nèi)容的問題,并且它具有記憶能力,能夠?qū)崿F(xiàn)真正的連貫性聊天。
在討論聊天機(jī)器人之前之前,先讓我們完成一些基礎(chǔ)性工作,比如設(shè)置一下openai的api key:
import os
import openai
import sys
sys.path.append('../..')
import panel as pn # GUI
pn.extension()
from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv()) # read local .env file
openai.api_key = os.environ['OPENAI_API_KEY']
?先前內(nèi)容回顧
之前我們介紹了Langchain在實(shí)現(xiàn)與外部數(shù)據(jù)對話的功能時(shí)需要經(jīng)歷下面的5個(gè)階段,它們分別是:Document Loading->Splitting->Storage->Retrieval->Output。下面我們通過代碼來簡單實(shí)現(xiàn)一下這5個(gè)階段的功能:
from langchain.vectorstores import Chroma
from langchain.embeddings.openai import OpenAIEmbeddings
#加載本地向量數(shù)據(jù)庫
persist_directory = 'docs/chroma/'
embedding = OpenAIEmbeddings()
vectordb = Chroma(persist_directory=persist_directory,
embedding_function=embedding)
#搜索與問題相關(guān)的文檔
question = "What are major topics for this class?"
docs = vectordb.similarity_search(question,k=3)
#查看搜索結(jié)果中的文檔數(shù)量
len(docs)
?這里我們在向量數(shù)據(jù)庫中搜索到3篇與問題相關(guān)的文檔,下面我們查看一下這3篇文檔:
docs
?下面我們來創(chuàng)建RetrievalQA,同時(shí)我們加入一個(gè)prompt的模板,在該prompt我們要求llm盡量用簡潔的語言來回答問題,并且不能編造答案,最后我們還要求llm在答案的結(jié)語上加上“thanks for asking!”,通過這個(gè)prompt模板llm能給出簡潔的格式化的答案:
from langchain.chat_models import ChatOpenAI
from langchain.prompts import PromptTemplate
# Build prompt
template = """Use the following pieces of context to answer the question at the end. \
If you don't know the answer, just say that you don't know, don't try to make up an answer. \
Use three sentences maximum. Keep the answer as concise as possible. \
Always say "thanks for asking!" at the end of the answer.
{context}
Question: {question}
Helpful Answer:"""
QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context", "question"],template=template,)
# Run chain
from langchain.chains import RetrievalQA
question = "Is probability a class topic?"
qa_chain = RetrievalQA.from_chain_type(llm=ChatOpenAI(temperature=0),
retriever=vectordb.as_retriever(),
return_source_documents=True,
chain_type_kwargs={"prompt": QA_CHAIN_PROMPT})
result = qa_chain({"query": question})
result["result"]
??????
?這里我們看到RetrievalQA返回了一個(gè)很簡潔的答案,并在最后附加了“thanks for asking!”,這符合我們對它的要求。
ConversationalRetrievalChain
到目前為止我們已經(jīng)創(chuàng)建好了RetrievalQA,可以實(shí)現(xiàn)對數(shù)據(jù)內(nèi)容的問答,不過這里會(huì)有一個(gè)問題,就是通過RetrievalQA創(chuàng)建的檢索問答鏈,它沒有記憶功能,它無法記住之前用戶已經(jīng)提出過問題,所以RetrievalQA不能實(shí)現(xiàn)連貫性的聊天問答。為了解決這個(gè)功能,我們可以通過創(chuàng)建ConversationalRetrievalChain,它會(huì)存儲(chǔ)每次聊天的歷史記錄,當(dāng)LLM在回答當(dāng)前問題的時(shí)候都會(huì)參考?xì)v史聊天記錄,這樣就可以實(shí)現(xiàn)連貫性的聊天:
為了保存么此用戶和LLM之間的聊天記錄,我們需要?jiǎng)?chuàng)建一個(gè)ConversationBufferMemory組件,該組件會(huì)自動(dòng)保存每一次用戶和LLM之間對話記錄。ConversationalRetrievalChain包含3給主要的參數(shù):
- llm: 語言模型,這里我們使用openai的“gpt-3.5-turbo”模型
- retriever:檢索器,這里我們由向量數(shù)據(jù)庫來創(chuàng)建檢索器
- memory:記憶力組件,這里我們使用ConversationBufferMemory
from langchain.chat_models import ChatOpenAI
from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationalRetrievalChain
#創(chuàng)建memory
memory = ConversationBufferMemory(
memory_key="chat_history",
return_messages=True
)
#創(chuàng)建ConversationalRetrievalChain
qa = ConversationalRetrievalChain.from_llm(
llm=ChatOpenAI(temperature=0),
retriever=vectordb.as_retriever(),
memory=memory
)
這里我們創(chuàng)建了ConversationalRetrievalChain的實(shí)例qa,接下來我們來實(shí)現(xiàn)連貫性的聊天,我們首先向LLM提出一個(gè)問題:概率是這門課的主題嗎?
question1="概率是這門課的主題嗎?"
result = qa({"question": question1})
print(result['answer'])
?接下來我們第二給問題:為什么需要先修課程呢?,這里需要說明的是該問題其實(shí)是銜接第一個(gè)問題的答案,如果我們的ConversationalRetrievalChain有記憶功能,那么它一定會(huì)知道這里的先修課程是指哪些課程,并且給出正確的回答:
question2 = "為什么需要先修課程呢?"
result = qa({"question": question2})
print(result['answer'])
?這里我們向LLM提出了2個(gè)問題,第一個(gè)問題是:概率是這門課的主題嗎?我們知道,我們的向量數(shù)據(jù)庫中存儲(chǔ)的是吳恩達(dá)老師著名的機(jī)器學(xué)習(xí)課程cs229的課程講義,因此課程中涉及到了一些概率的基礎(chǔ)知識(shí),那么接下來提出的第二給問題:為什么需要先修課程呢?該問題其實(shí)是銜接第一個(gè)問題的答案,要回答該問題必須要知道這里的先修課程是指哪些課程,因?yàn)長LM在回答第一個(gè)問題的時(shí)候已經(jīng)明確告知用戶概率是這門課的一個(gè)主題,那么概率也就是這門課的先修課程,這里我們看到ConversationalRetrievalChain在回答第二給問題的時(shí)候已經(jīng)參考了之前的歷史聊天記錄,因此它給出了合理的答案。
創(chuàng)建聊天機(jī)器人
下面我們把Langchain在實(shí)現(xiàn)與外部數(shù)據(jù)對話的功能的5個(gè)階段所有的內(nèi)容整合起來,然后建一個(gè)真正意義上的聊天機(jī)器人,這里我們在jupyter notebook中使用panel組件來創(chuàng)建一個(gè)GUI的聊天對話界面:
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.text_splitter import CharacterTextSplitter, RecursiveCharacterTextSplitter
from langchain.vectorstores import DocArrayInMemorySearch
from langchain.document_loaders import TextLoader
from langchain.chains import RetrievalQA, ConversationalRetrievalChain
from langchain.memory import ConversationBufferMemory
from langchain.chat_models import ChatOpenAI
from langchain.document_loaders import TextLoader
from langchain.document_loaders import PyPDFLoader
import panel as pn
import param
def load_db(file, chain_type, k):
# load documents
loader = PyPDFLoader(file)
documents = loader.load()
# split documents
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=150)
docs = text_splitter.split_documents(documents)
# define embedding
embeddings = OpenAIEmbeddings()
# create vector database from data
db = DocArrayInMemorySearch.from_documents(docs, embeddings)
# define retriever
retriever = db.as_retriever(search_type="similarity", search_kwargs={"k": k})
# create a chatbot chain. Memory is managed externally.
qa = ConversationalRetrievalChain.from_llm(
llm=ChatOpenAI(temperature=0),
chain_type=chain_type,
retriever=retriever,
return_source_documents=True,
return_generated_question=True,
)
return qa
class cbfs(param.Parameterized):
chat_history = param.List([])
answer = param.String("")
db_query = param.String("")
db_response = param.List([])
def __init__(self, **params):
super(cbfs, self).__init__( **params)
self.panels = []
self.loaded_file = "docs/cs229_lectures/MachineLearning-Lecture01.pdf"
self.qa = load_db(self.loaded_file,"stuff", 4)
def call_load_db(self, count):
if count == 0 or file_input.value is None: # init or no file specified :
return pn.pane.Markdown(f"Loaded File: {self.loaded_file}")
else:
file_input.save("temp.pdf") # local copy
self.loaded_file = file_input.filename
button_load.button_style="outline"
self.qa = load_db("temp.pdf", "stuff", 4)
button_load.button_style="solid"
self.clr_history()
return pn.pane.Markdown(f"Loaded File: {self.loaded_file}")
def convchain(self, query):
if not query:
return pn.WidgetBox(pn.Row('User:', pn.pane.Markdown("", width=600)), scroll=True)
result = self.qa({"question": query, "chat_history": self.chat_history})
self.chat_history.extend([(query, result["answer"])])
self.db_query = result["generated_question"]
self.db_response = result["source_documents"]
self.answer = result['answer']
self.panels.extend([
pn.Row('User:', pn.pane.Markdown(query, width=600)),
pn.Row('ChatBot:', pn.pane.Markdown(self.answer, width=600, style={'background-color': '#F6F6F6'}))
])
inp.value = '' #clears loading indicator when cleared
return pn.WidgetBox(*self.panels,scroll=True)
@param.depends('db_query ', )
def get_lquest(self):
if not self.db_query :
return pn.Column(
pn.Row(pn.pane.Markdown(f"Last question to DB:", styles={'background-color': '#F6F6F6'})),
pn.Row(pn.pane.Str("no DB accesses so far"))
)
return pn.Column(
pn.Row(pn.pane.Markdown(f"DB query:", styles={'background-color': '#F6F6F6'})),
pn.pane.Str(self.db_query )
)
@param.depends('db_response', )
def get_sources(self):
if not self.db_response:
return
rlist=[pn.Row(pn.pane.Markdown(f"Result of DB lookup:", styles={'background-color': '#F6F6F6'}))]
for doc in self.db_response:
rlist.append(pn.Row(pn.pane.Str(doc)))
return pn.WidgetBox(*rlist, width=600, scroll=True)
@param.depends('convchain', 'clr_history')
def get_chats(self):
if not self.chat_history:
return pn.WidgetBox(pn.Row(pn.pane.Str("No History Yet")), width=600, scroll=True)
rlist=[pn.Row(pn.pane.Markdown(f"Current Chat History variable", styles={'background-color': '#F6F6F6'}))]
for exchange in self.chat_history:
rlist.append(pn.Row(pn.pane.Str(exchange)))
return pn.WidgetBox(*rlist, width=600, scroll=True)
def clr_history(self,count=0):
self.chat_history = []
return
cb = cbfs()
file_input = pn.widgets.FileInput(accept='.pdf')
button_load = pn.widgets.Button(name="Load DB", button_type='primary')
button_clearhistory = pn.widgets.Button(name="Clear History", button_type='warning')
button_clearhistory.on_click(cb.clr_history)
inp = pn.widgets.TextInput( placeholder='Enter text here…')
bound_button_load = pn.bind(cb.call_load_db, button_load.param.clicks)
conversation = pn.bind(cb.convchain, inp)
jpg_pane = pn.pane.Image( './img/convchain.jpg')
tab1 = pn.Column(
pn.Row(inp),
pn.layout.Divider(),
pn.panel(conversation, loading_indicator=True, height=300),
pn.layout.Divider(),
)
tab2= pn.Column(
pn.panel(cb.get_lquest),
pn.layout.Divider(),
pn.panel(cb.get_sources ),
)
tab3= pn.Column(
pn.panel(cb.get_chats),
pn.layout.Divider(),
)
tab4=pn.Column(
pn.Row( file_input, button_load, bound_button_load),
pn.Row( button_clearhistory, pn.pane.Markdown("Clears chat history. Can use to start a new topic" )),
pn.layout.Divider(),
pn.Row(jpg_pane.clone(width=400))
)
dashboard = pn.Column(
pn.Row(pn.pane.Markdown('# ChatWithYourData_Bot')),
pn.Tabs(('Conversation', tab1), ('Database', tab2), ('Chat History', tab3),('Configure', tab4))
)
#啟動(dòng)聊天應(yīng)用程序
dashboard
?總結(jié)
?今天我們學(xué)習(xí)了如何開發(fā)一個(gè)具有記憶能力的個(gè)性化問答機(jī)器人,所謂個(gè)性化是指該機(jī)器人可以針對用戶數(shù)據(jù)的內(nèi)容進(jìn)行問答,我們在實(shí)現(xiàn)該機(jī)器人時(shí)使用了ConversationalRetrievalChain組件,它是一個(gè)具有記憶能力的檢索鏈,也是機(jī)器人的核心組件。希望今天的內(nèi)容對大家有所幫助!
參考資料
Overview — Panel v1.2.1
Welcome to Param! — param v1.13.0文章來源:http://www.zghlxwxcb.cn/news/detail-635121.html
https://github.com/sophiamyang/tutorials-LangChain文章來源地址http://www.zghlxwxcb.cn/news/detail-635121.html
到了這里,關(guān)于使用langchain與你自己的數(shù)據(jù)對話(五):聊天機(jī)器人的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!