上一篇文章實作了 Memory 的對話功能,有將對話紀錄存在變數中,也有存在 Upstash-Redis 的雲端資料庫。那今天第 21 天,來統整一下之前用過的技術,包含 API 呼叫 LLM 和 Embeddings Model、Qdrant 向量資料庫、Upstash-Redis 儲存對話紀錄、RAG、LCEL,將以上的技術皆納入今天的實作當中,當作是這 21 天的小成果!
# 匯入套件
from langchain_ffm import ChatFormosaFoundationModel, FFMEmbedding
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_qdrant import QdrantVectorStore
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder, HumanMessagePromptTemplate, SystemMessagePromptTemplate, PromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_community.chat_message_histories import UpstashRedisChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from qdrant_client import QdrantClient
from qdrant_client.http import models
from qdrant_client.http.models import Distance
from operator import itemgetter
from dotenv import load_dotenv
load_dotenv()
import os
# 選擇模型和連線向量資料庫
llm = ChatFormosaFoundationModel(model="ffm-llama3-70b-chat", temperature=0.01)
embeddings = FFMEmbedding(model='ffm-embedding')
client = QdrantClient(url="http://localhost:6333")
collections_name = "ithome-RAG-With-Memory"
# 若 collection 存在則刪除 (現在沒有recreate函數)
if client.collection_exists(collection_name=collections_name):
client.delete_collection(collection_name=collections_name)
else:
pass
# 建立一個新的 collections
client.create_collection(
collection_name=collections_name,
vectors_config=
models.VectorParams(
size=1536,
distance=Distance.COSINE,
),
)
# langchain 連線 Qdrant 已存在的 collections
qdrant_vector_store = QdrantVectorStore.from_existing_collection(
url="http://localhost:6333",
collection_name=collections_name,
embedding=embeddings,
)
# 切割 pdf 內容的函數
def split_pdf(pdf_path, pdf_chunk_size, pdf_chunk_overlap):
pdf_loader = PyPDFLoader(pdf_path)
pdf_docs = pdf_loader.load()
pdf_text_splitter = RecursiveCharacterTextSplitter(chunk_size=pdf_chunk_size, chunk_overlap=pdf_chunk_overlap)
pdf_split = pdf_text_splitter.split_documents(pdf_docs)
return pdf_split
# 取得切割結果並匯入資料庫
pdf_data = split_pdf("https://web.metro.taipei/QRCode/Regulations%20for%20Use%20of%20the%20Taipei%20Metro%20System-Chinese.pdf", pdf_chunk_size=500, pdf_chunk_overlap=100)
qdrant_vector_store.add_documents(documents=pdf_data)
# 指定 retriever
retriever = qdrant_vector_store.as_retriever()
# RAG 的 prompt
qa_template = """你是一位回答問題的助手,使用以下檢索到的內容來回答問題。如果你不知道答案,就說你不知道。最多使用三個句子並保持答案簡潔。
{context}
"""
prompt = ChatPromptTemplate.from_messages(
[
SystemMessagePromptTemplate.from_template(qa_template),
MessagesPlaceholder(variable_name="history"),
HumanMessagePromptTemplate.from_template("{question}")
]
)
# 將所有東西都 Chain 起來
chain = (
RunnablePassthrough.assign(context = itemgetter("question") | retriever)
| prompt
| llm
)
# 聊天會話管理功能,可以管理多個對話
def get_session_history(session_id: str) -> BaseChatMessageHistory:
return UpstashRedisChatMessageHistory(
url=os.environ['UPSTASH_REDIS_REST_URL'],
token=os.environ['UPSTASH_REDIS_REST_TOKEN'],
session_id=session_id
)
# 將聊天歷史與 LLM 串在一起
with_message_history = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key="question",
history_messages_key="history",
)
# 定義聊天函數
def chat_with_ffm(user_input, session_id):
response = with_message_history.invoke(
{"question": user_input},
config={"configurable": {"session_id": session_id}},
)
return response.content
# 開始與 AI 對話
if __name__ == "__main__":
session_id = "MRT"
while True:
user_input = input("You: ")
if user_input.lower() == "exit":
break
elif user_input.lower() == "clear":
session_id = input("Enter a new session ID: ")
user_input = input("You: ")
response = chat_with_ffm(user_input, session_id)
print(f"Assiatant: {response}")
程式碼結果探討 🧐:
RunnableParallel({"context": retriever, "question": RunnablePassthrough()})
,但因為 Memory 的關係,invoke 傳入的參數就不只一個,所以沒辦法使用這個方式。RunnablePassthrough.assign(context = itemgetter("question") | retriever)
這個方式就是從傳入的參數中取得 question 的部分,再傳遞給 Retriever 去檢索 question 對應的資料。今天的實作成果結合了之前的生成式 AI 的技術和工具,我自己是覺得這些工具都是生成式 AI 必備的技術,尤其是 LangChain 框架,但 LangChain 缺點也很多,是讓人又愛又恨,以下我統整幾個我自己覺得的 LangChain 優缺點:
RunnableWithMessageHistory
在 langchain_core
底下,但是後面的 runnable 和 history 真的不是這麼好記,每次使用的時候都要先去查一下。來推薦一個我很喜歡也是我希望成為像他一樣厲害的一位全端工程師,甚至還會寫遊戲程式,真的是好好膜拜。他就是 陳南宗 ,連結是他的 FB 粉專,這是他的自我介紹,我只能說除了佩服還是佩服,我時不時就會看看他有沒有分享一些 AI 相關的新技術或者工具,分享給大家~