參考自
- 基于LangChain+LLM的本地知識庫問答:從企業單文檔問答到批量文檔問答
- datawhale的llm-universe
作者現在在datawhale夏令營的大模型應用開發這個班中,作為一個小白,為了能為團隊做出一點貢獻,現在就要開始學習怎么使用langchain的api來實現基本的功能!
隨便談談
由于科大訊飛的星火杯已經為我們提供了 星火大模型 的API,我也不好進行模型調參,現在能做的就是開展prompt工程
,結合RAG
,給到模型優質的輸入信息,幫助它正確回答我提出的疑問和要求。我首先學的就是這兩個。
我一開始的想法是做本地知識庫,那么模型是怎么知道這些知識呢?我們知道一個人的知識儲備,可以來自他的積累,也可以來自他現在手里正翻著的書(或者當場百度:))。大模型也一樣。我們可以在訓練大模型時讓他學習龐大的知識,也可以在提問一些小眾知識時附加可參考的內容(比如在詢問一個rust的第三方庫怎么用時,順帶把這個庫的文檔文本一起給他,讓他參考這些文檔進行回答)
- 參數知識:在訓練期間學習到的知識,隱式存儲在神經網絡的權重中。
- 非參數知識:存儲在外部知識源中,例如向量數據庫。
當然,直接傳整篇文檔的方式比較粗暴,可以事先將全篇文檔放入向量數據庫或知識圖譜(統稱為知識庫)。那么一次問答的過程為:首先用戶提問請回答我的xx問題
,然后大模型根據用戶的提問去知識庫中匹配并獲取相關的參考資料,然后再將用戶輸入改為根據 xxxxx 這些參考資料,請回答我的xx問題
并傳入大模型。
一步步基于langchain實現訊飛大模型的調用和RAG
主要參考自datawhale的教程https://github.com/datawhalechina/llm-universe/tree/main/notebook,確實比較詳細
我實操的大概過程為:
- 調用星火大模型
- 讀取Markdown或pdf作為本地知識,并解析出其中的文本,并進行數據清洗
- 對解析出的文本進行文本切分。因為單個文檔的長度往往會超過模型支持的上下文,導致檢索得到的知識太長超出模型的處理能力
- 調用星火的文本向量化接口
- 使用Chroma存儲向量并檢索,即建立本地知識庫
- 使用template,并使用langchain的
LCEL
語法實現一條鏈式處理,該鏈將獲取輸入變量,將這些變量傳遞給提示模板以創建提示,將提示傳遞給語言模型,然后通過(可選)輸出解析器傳遞輸出。 - 將"知識庫檢索"這一過程加入處理鏈中,實現大模型鏈接本地知識庫
- 基于Memory模塊讓大模型能使用到歷史對話,實現帶上下文的對話
- streamlit的教學我沒看,目前考慮使用Gradio
以上過程均基于langchain的api。
我這里只放一些重點內容,對詳細過程感興趣的同學可以跟著教程繼續學習,我就不粘貼一遍教程里的代碼了
RAG和微調的對比
特征比較 | RAG | 微調 |
---|---|---|
知識更新 | 直接更新檢索知識庫,無需重新訓練。信息更新成本低,適合動態變化的數據。 | 通常需要重新訓練來保持知識和數據的更新。更新成本高,適合靜態數據。 |
外部知識 | 擅長利用外部資源,特別適合處理文檔或其他結構化/非結構化數據庫。 | 將外部知識學習到 LLM 內部。 |
數據處理 | 對數據的處理和操作要求極低。 | 依賴于構建高質量的數據集,有限的數據集可能無法顯著提高性能。 |
模型定制 | 側重于信息檢索和融合外部知識,但可能無法充分定制模型行為或寫作風格。 | 可以根據特定風格或術語調整 LLM 行為、寫作風格或特定領域知識。 |
可解釋性 | 可以追溯到具體的數據來源,有較好的可解釋性和可追蹤性。 | 黑盒子,可解釋性相對較低。 |
計算資源 | 需要額外的資源來支持檢索機制和數據庫的維護。 | 依賴高質量的訓練數據集和微調目標,對計算資源的要求較高。 |
推理延遲 | 增加了檢索步驟的耗時 | 單純 LLM 生成的耗時 |
降低幻覺 | 通過檢索到的真實信息生成回答,降低了產生幻覺的概率。 | 模型學習特定領域的數據有助于減少幻覺,但面對未見過的輸入時仍可能出現幻覺。 |
倫理隱私 | 檢索和使用外部數據可能引發倫理和隱私方面的問題。 | 訓練數據中的敏感信息需要妥善處理,以防泄露。 |
LangChain
封裝了很多模型的調用方式以及工具比如chain、memory等
https://python.langchain.com/v0.2/docs/integrations/llms/
Prompt
在 ChatGPT 推出并獲得大量應用之后,Prompt 開始被推廣為給大模型的所有輸入。即,我們每一次訪問大模型的輸入為一個 Prompt,而大模型給我們的返回結果則被稱為 Completion。
Temperature
LLM 生成是具有隨機性的,我們一般可以通過控制 temperature
參數來控制 LLM 生成結果的隨機性與創造性。
Temperature 一般取值在 0~1 之間,當取值較低接近 0 時,預測的隨機性會較低,產生更保守、可預測的文本,不太可能生成意想不到或不尋常的詞。當取值較高接近 1 時,預測的隨機性會較高,所有詞被選擇的可能性更大,會產生更有創意、多樣化的文本,更有可能生成不尋常或意想不到的詞。
對于不同的問題與應用場景,我們可能需要設置不同的 temperature。例如,在個人知識庫助手項目中,我們一般將 temperature 設置為 0,從而保證助手對知識庫內容的穩定使用,規避錯誤內容、模型幻覺;在產品智能客服、科研論文寫作等場景中,我們同樣更需要穩定性而不是創造性;但在個性化 AI、創意營銷文案生成等場景中,我們就更需要創意性,從而更傾向于將 temperature 設置為較高的值。
System Prompt
在使用 ChatGPT API 時,你可以設置兩種 Prompt:一種是 System Prompt,該種 Prompt 內容會在整個會話過程中持久地影響模型的回復,且相比于普通 Prompt 具有更高的重要性;另一種是 User Prompt,這更偏向于我們平時提到的 Prompt,即需要模型做出回復的輸入。
System Prompt 一般在一個會話中僅有一個。在通過 System Prompt 設定好模型的人設或是初始設置后,我們可以通過 User Prompt 給出模型需要遵循的指令。
例如,當我們需要一個幽默風趣的個人知識庫助手,并向這個助手提問我今天有什么事時,可以構造如下的 Prompt:
{"system prompt": "你是一個幽默風趣的個人知識庫助手,可以根據給定的知識庫內容回答用戶的提問,注意,你的回答風格應是幽默風趣的","user prompt": "我今天有什么事務?"
}
Prompt工程
可以理解為使用結構化的提問方式,向模型傳入更詳細的提問內容,引導大模型進行更精確和正確的回答
具體可參考https://github.com/datawhalechina/llm-universe/blob/main/notebook/C2%20%E4%BD%BF%E7%94%A8%20LLM%20API%20%E5%BC%80%E5%8F%91%E5%BA%94%E7%94%A8/3.%20Prompt%20Engineering.ipynb
此次開發用到的官方文檔
- langchain中使用sparkllm
- langchain中使用spark的文本向量化
- langchain中解析文件中的文本
- langchain中使用Chroma
還是貼個代碼
知識庫為一篇Markdown https://github.com/sunface/rust-course/blob/main/src/advance/macro.md
這里的代碼參考datawhale的代碼,只實現了鏈接知識庫和結合上下文對話,沒有用到LCEL。深入源碼以及看官方文檔后發現,LangChain非常多的API棄用或將被棄用,API改動這么頻繁,而且有些API都不好在IDE里跳到源碼,難怪langchain的爭議這么大。
import os
import re
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.llms.sparkllm import SparkLLM
from langchain_community.embeddings import SparkLLMTextEmbeddings
from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain.prompts.chat import ChatPromptTemplate,SystemMessagePromptTemplate,HumanMessagePromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain.prompts import PromptTemplate
from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationalRetrievalChain# 通過環境變量的方式設置秘鑰,具體的key-value可以在IDE內點進源碼查看
def load_env():# 星火認知大模型Spark Max的URL值,其他版本大模型URL值請前往文檔(https://www.xfyun.cn/doc/spark/Web.html)查看#星火認知大模型調用秘鑰信息,請前往訊飛開放平臺控制臺(https://console.xfyun.cn/services/bm35)查看#星火認知大模型Spark Max的domain值,其他版本大模型domain值請前往文檔(https://www.xfyun.cn/doc/spark/Web.html)查看os.environ["IFLYTEK_SPARK_API_URL"] = "wss://spark-api.xf-yun.com/v3.5/chat"os.environ["IFLYTEK_SPARK_API_KEY"] = ""os.environ["IFLYTEK_SPARK_API_SECRET"] = ""os.environ["IFLYTEK_SPARK_APP_ID"] = ""os.environ["IFLYTEK_SPARK_LLM_DOMAIN"] = "generalv3.5"# 文本向量化os.environ["SPARK_APP_ID"] = ""os.environ["SPARK_API_KEY"] = ""os.environ["SPARK_API_SECRET"] = ""# 使用UnstructuredMarkdownLoader讀取時似乎不能讀emoji
# 此時需要預先處理文件,再使用API解析無emoji的Markdown文件
def remove_emojis(text):try:co = re.compile(u'[\U00010000-\U0010ffff]')except re.error:co = re.compile(u'[\uD800-\uDBFF][\uDC00-\uDFFF]')no_emojis_text = co.sub(r'', text)return no_emojis_text# 長文本切分為多個小文檔
def text_split(doc):# 知識庫中單段文本長度CHUNK_SIZE = 500# 知識庫中相鄰文本重合長度OVERLAP_SIZE = 50# 使用遞歸字符文本分割器text_splitter = RecursiveCharacterTextSplitter(chunk_size=CHUNK_SIZE,chunk_overlap=OVERLAP_SIZE)return text_splitter.split_documents(doc)def prepare_knowledge():# 此篇文章無emojifiles = ["macro.md"]md_pages = []for f in files:loader = UnstructuredMarkdownLoader(f)md_page = loader.load()# 打印這個文檔來自哪個的文件名# source = md_page.metadata['source']# print(source)# 刪除無必要的連續換行md_page[0].page_content = md_page[0].page_content.replace('\n\n', '\n')md_pages.extend(md_page)# 文本切分docs = text_split(md_pages)# 實例化星火文本向量化接口的調用工具embeddings = SparkLLMTextEmbeddings()# 可以直接調用來向量化文本# embeddings.aembed_query(text)/aembed_documents(doc)# 初始化一個Chroma向量數據庫,讓Chroma將文檔向量化# 并將數據庫文件和向量數據保存到文件夾中vectordb = Chroma.from_documents(documents=docs,embedding=embeddings,persist_directory='./vector')vectordb.persist()if __name__ == '__main__':# 設置環境變量load_env()# 實例化星火大模型調用工具llm = SparkLLM(temperature=0.95)# 初始化知識庫prepare_knowledge()# 加載本地知識庫embeddings = SparkLLMTextEmbeddings()vector_db = Chroma(persist_directory="./vector", embedding_function=embeddings)# 對話歷史記錄memory = ConversationBufferMemory(memory_key="chat_history",return_messages=True, # 將以消息列表的形式返回聊天記錄,而不是單個字符串)# 模版system_template = """使用提供的上下文和聊天記錄回答用戶的問題。不知道也不要編造答案,回答盡量簡要。并且總是在回答的最后說“謝謝你的提問!”----------------CONTEXT:{context}CHAT HISTORY:{chat_history}USER QUESTION:{question}"""messages = [SystemMessagePromptTemplate.from_template(system_template),HumanMessagePromptTemplate.from_template("{question}")]qa_prompt = ChatPromptTemplate.from_messages(messages)# 創建問答鏈,為其添加檢索知識庫和歷史記錄的功能qa = ConversationalRetrievalChain.from_llm(llm,retriever=vector_db.as_retriever(),combine_docs_chain_kwargs={'prompt': qa_prompt},memory=memory)print("第一次問答:")question = "我可以在你這里學習到關于提示工程的知識嗎?"result = qa({"question": question})print(result['answer'])print("第二次問答:")question = "為什么?"result = qa({"question": question})print(result['answer'])
由于突然文本向量化接口突然報錯,上述代碼我這里測試不了構建知識庫了。但好在可以加載之前構建好的知識庫來測試后面的代碼。代碼總體應該沒問題,之后再看看
Request error: 11202, {‘header’: {‘code’: 11202, ‘message’: ‘licc failed’, ‘sid’: ‘emb000fc090@dx1907452bed6738d882’}}
Request error: 11202, {‘header’: {‘code’: 11202, ‘message’: ‘licc failed’, ‘sid’: ‘emb000ebcb7@dx1907452bf547020882’}}
TODO
- 怎么持久化以及加載歷史信息,這篇博客到時候可以參考:【LangChain】對話式問答(Conversational Retrieval QA)
- 怎么將知識圖譜作為知識庫
- 評測和優化