一、簡介
我們之前完成了一個簡易的聊天機器人,但是還留下了一些問題沒有解決,比如如何開啟新的會話。如何切換session_id,如何把對話做成流式的輸出。這些我們就會在今天來完成。
二、關于新的會話和session_id
from dotenv import load_dotenv
from langchain_ollama import ChatOllama
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import ElasticsearchChatMessageHistoryfrom streamlit import streamlit as st# load env file
load = load_dotenv("./.env")# 無密碼的elasticsearch配置
es_url = "http://localhost:9200"
# 存儲的索引,我們不用預先創建索引,因為說實話我也不知道字段,langchain會創建,并且自動映射字段
index_name = "chat_history"# init llm
llm = ChatOllama(base_url = "http://127.0.0.1:11434",model = "huihui_ai/deepseek-r1-abliterated:14b",temperature = 0.5,num_predict = 10000
)# 構建ElasticsearchChatMessageHistory
def get_session_history(session_id: str) :return ElasticsearchChatMessageHistory(index=index_name,session_id=session_id,es_url=es_url,ensure_ascii=False)session_id = "levi"
st.title("你好,這里是橘子GPT,我是小橘")
st.write("請把您的問題輸入,小橘會認真回答的哦。")
# 添加一個輸入框,用戶自己輸入來替代默認的
session_id = st.text_input("請輸入一個session_id,否則我們將使用默認值levi",session_id)
# 添加一個按鈕,觸發按鈕開啟新的會話,并且刪除上下文記錄
isClickButton:bool = st.button("點擊按鈕,開啟新的對話")
# query input
user_prompt = st.chat_input("我是小橘,請輸入你的問題吧")if isClickButton:# 清除es中存儲的上下文記錄,這個是物理刪除delete_by_queryget_session_history(session_id).clear()# 清除當前會話的上下文st.session_state.chat_history = []# 如果沒有就創建,不要每次都建立新的
if 'chat_history' not in st.session_state:st.session_state.chat_history = []
# 遍歷里面的內容,取出來存進去的每一組role 和 content
for message in st.session_state.chat_history :# 通過取出來的信息,構建st.chat_message,不同的角色會有不同的ui樣式,這里就是做這個的with st.chat_message(message['role']):# 把內容展示出來st.markdown(message['content'])template = ChatPromptTemplate.from_messages([('human',"{prompt}"),('placeholder',"{history}")
])
chain = template | llm | StrOutputParser()
chain_with_history = RunnableWithMessageHistory(chain,get_session_history,input_messages_key="prompt",history_messages_key="history",
)if user_prompt :response = chain_with_history.invoke({"prompt": user_prompt},config={"configurable": {"session_id": session_id}})# 保存歷史,上面用來遍歷顯示,避免后面覆蓋前面的顯示st.session_state.chat_history.append({'role':'user','content':user_prompt})with st.chat_message('user'):st.markdown(user_prompt)# 保存歷史,上面用來遍歷顯示,避免后面覆蓋前面的顯示st.session_state.chat_history.append({'role':'assistant','content':response})with st.chat_message('assistant'):st.markdown(response)
這樣就實現了需求了,可以直接跑一下看看。
三、關于流式對話
流式對話其實說白了就是回復的時候一個字一個字的顯示,表現為流的樣子,而不是一次性返回一大堆。
from dotenv import load_dotenv
from langchain_ollama import ChatOllama
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import ElasticsearchChatMessageHistoryfrom streamlit import streamlit as st# load env file
load = load_dotenv("./.env")# 無密碼的elasticsearch配置
es_url = "http://localhost:9200"
# 存儲的索引,我們不用預先創建索引,因為說實話我也不知道字段,langchain會創建,并且自動映射字段
index_name = "chat_history"
# streamlit code
session_id = "levi"# init llm
llm = ChatOllama(base_url = "http://127.0.0.1:11434",model = "huihui_ai/deepseek-r1-abliterated:14b",temperature = 0.5,num_predict = 10000
)
# 構建ElasticsearchChatMessageHistory
def get_session_history(session_id: str) :return ElasticsearchChatMessageHistory(index=index_name,session_id=session_id,es_url=es_url,ensure_ascii=False)st.title("你好,這里是橘子GPT,我是小橘")
st.write("請把您的問題輸入,小橘會認真回答的哦。")
# 設置一個輸入框,用戶輸入的內容來替代默認的session_id。
session_id = st.text_input("請輸入一個session_id,否則我們將使用默認值levi",session_id)
# 添加一個按鈕,點擊按鈕的時候清空歷史會話
isClickButton:bool = st.button("點擊按鈕,開啟新的對話")
if isClickButton:# 注意這一行會去調用es客戶端delete_by_query物理刪除es中的上下文數據,最好自己定制,不要直接用get_session_history(session_id).clear()# 清除當前窗口的上下文記錄st.session_state.chat_history = []# query input
user_prompt = st.chat_input("我是小橘,請輸入你的問題吧")# 構建langchain執行
def invoke_history_by_stream(chain,prompt,session_id):chain_with_history = RunnableWithMessageHistory(chain,get_session_history,input_messages_key = prompt,history_messages_key="history",)# 以stream流的方式執行responseStream = chain_with_history.stream({"prompt": prompt},config={"configurable": {"session_id": session_id}})# 遍歷返回,逐個回復for response in responseStream:yield response# 如果沒有就創建,不要每次都建立新的
if 'chat_history' not in st.session_state:st.session_state.chat_history = []
# 遍歷里面的內容,取出來存進去的每一組role 和 content
for message in st.session_state.chat_history :# 通過取出來的信息,構建st.chat_message,不同的角色會有不同的ui樣式,這里就是做這個的with st.chat_message(message['role']):# 把內容展示出來st.markdown(message['content'])template = ChatPromptTemplate.from_messages([('human',"{prompt}"),('placeholder',"{history}")
])
chain = template | llm | StrOutputParser()# 如果感知到輸入
if user_prompt :# 保存歷史,上面用來遍歷顯示,避免后面覆蓋前面的顯示st.session_state.chat_history.append({'role':'user','content':user_prompt})with st.chat_message('user'):st.markdown(user_prompt)with st.chat_message('assistant'):# 以流式的方式渲染答案streamResp = st.write_stream(invoke_history_by_stream(chain,user_prompt,session_id))# 保存歷史,上面用來遍歷顯示,避免后面覆蓋前面的顯示st.session_state.chat_history.append({'role': 'assistant', 'content': streamResp})
這就是流的代碼,可以執行一下。
四、優化一版
我們再來優化一下樣式。這個可以有可以不做,最好交給前端來做,streamlit的ui太生硬了。加了幾個模板占位的替換。
from dotenv import load_dotenv
from langchain_community.chat_message_histories import ElasticsearchChatMessageHistory
from langchain_ollama import ChatOllama
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.prompts import MessagesPlaceholder, ChatPromptTemplatefrom streamlit import streamlit as stload = load_dotenv("./.env")
es_url = "http://localhost:9200"
index_name = "chat_history"
def get_session_history(session_id: str) :return ElasticsearchChatMessageHistory(index=index_name,session_id=session_id,es_url=es_url,ensure_ascii=False)
# init llm
llm = ChatOllama(base_url = "http://127.0.0.1:11434",model = "huihui_ai/deepseek-r1-abliterated:14b",temperature = 0.5,num_predict = 10000
)user_id = "levi"with st.sidebar:st.image("./orange.png", width=150)user_id = st.text_input("輸入你的id", user_id)role = st.radio("你想獲得什么級別的回答呢?", ["初學者", "專家", "大佬"], index=0)if st.button("清空歷史上下文,開啟新的對話"):st.session_state.chat_history = []get_session_history(user_id).clear()st.markdown("""<div style='display: flex; height: 70vh; justify-content: center; align-items: center;'><h2>請問你需要什么幫助呢?</h2></div>""",unsafe_allow_html=True
)if 'chat_history' not in st.session_state:st.session_state.chat_history = []for message in st.session_state.chat_history:with st.chat_message(message['role']):st.markdown(message['content'])template = ChatPromptTemplate.from_messages([MessagesPlaceholder(variable_name="history"),('system', f"你作為一個 {role} 級別的人來回答這個問題"),('human', "{prompt}")
])chain = template | llm | StrOutputParser()def invoke_history(chain, session_id, prompt):history = RunnableWithMessageHistory(chain, get_session_history,input_messages_key="prompt",history_messages_key="history")for response in history.stream({"prompt": prompt},config={"configurable": {"session_id": session_id}}):yield responseprompt = st.chat_input("輸入你的問題,小橘會為你回答。")if prompt:st.session_state.chat_history.append({'role': 'user', "content": prompt})with st.chat_message('user'):st.markdown(prompt)with st.chat_message('assistant'):streamResponse = st.write_stream(invoke_history(chain, user_id, prompt))st.session_state.chat_history.append({'role': 'assistant', "content": streamResponse})